govuk-rspec-helpers 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2688889ddc616f1479c43451e7cf7eb79cf2f1ea15cf56eaf2de361dfff89d51
4
+ data.tar.gz: 6a5b5fe732f6025d57884b152ab0898030b27c8d3fd17ab1145d14fbc8223824
5
+ SHA512:
6
+ metadata.gz: 5061ab29b4267c0b87cdcb5ad862d1c60e94eaf85163c8d036cb4bd98efe8d1f628c95ccd0b6cb37b584ab23d03ae3d32b3bb942e06fbaa107dce7a14e3c3e3e
7
+ data.tar.gz: b613d4f0f68a69b9876cc8d19ef55ea19bcb2ae8952b1373d78a8c0b14094920f6228906c7c7a383c05d5a44ab7b72596a9c51fdc8e74753b45d5d806eb311e9
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Frankie Roberto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # GOV.UK RSpec Helpers
2
+
3
+ This gem providers a set of helpers to make it easier to test GOV.UK services using the [RSpec](https://rspec.info) framework.
4
+
5
+ This is a pre-release. Feedback is welcome.
6
+
7
+ See [documentation](https://x-govuk.github.io/govuk-rspec-helpers/).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,167 @@
1
+ module GovukRSpecHelpers
2
+ class GovukCheckbox
3
+
4
+ attr_reader :page, :label_text, :hint_text
5
+
6
+ def initialize(page:, label_text:, hint_text:)
7
+ @label_text = label_text
8
+ @hint_text = hint_text
9
+ @page = page
10
+ end
11
+
12
+ def check
13
+ set_checked(true)
14
+ end
15
+
16
+ def uncheck
17
+ set_checked(false)
18
+ end
19
+
20
+ private
21
+
22
+ def set_checked(checked)
23
+ labels = page.all('label', text: label_text, exact_text: true, normalize_ws: true)
24
+
25
+ if labels.size == 0
26
+ check_for_input_id_match
27
+
28
+ raise "Unable to find label with the text \"#{label_text}\""
29
+ elsif labels.size > 1
30
+ raise "Found #{labels.size} labels with the same text. Checkbox labels should be unique within a fieldset."
31
+ end
32
+
33
+ @label = labels.first
34
+
35
+ check_label_has_a_for_attribute
36
+
37
+ inputs_matching_label = page.all(id: @label[:for])
38
+
39
+ if inputs_matching_label.size == 1
40
+ @input = inputs_matching_label.first
41
+ elsif inputs_matching_label.size == 0
42
+ raise "Found the label but it is not associated with an checkbox, did not find a checkbox with the ID \"#{@label[:for]}\"."
43
+ else
44
+ raise "Found #{inputs_matching_label.size} elements with id=\"#{@label[:for]}\". IDs must be unique."
45
+ end
46
+
47
+ check_input_type_is_radio
48
+
49
+ check_label_classes
50
+ check_input_class
51
+
52
+ hints = @label.ancestor('.govuk-checkboxes__item')
53
+ .all('.govuk-hint', text: hint_text, exact_text: true, normalize_ws: true)
54
+
55
+ @hint = hints.first
56
+
57
+ check_for_hint if hint_text
58
+ check_that_hint_is_associated_with_input if @hint
59
+ check_that_checkbox_not_checked if checked
60
+ check_that_checkbox_is_checked if !checked
61
+
62
+ @label.click
63
+ end
64
+
65
+ def check_for_input_id_match
66
+ inputs = page.all('input', id: label_text)
67
+
68
+ if inputs.size > 0
69
+
70
+ matching_labels = page.all("label[for=#{label_text}]")
71
+
72
+ if matching_labels.size > 0
73
+ raise "Use the full label text \"#{matching_labels.first.text}\" instead of the input ID"
74
+ end
75
+ end
76
+ end
77
+
78
+ def check_label_has_a_for_attribute
79
+ if !@label[:for]
80
+ raise 'Found the label but it is not associated with an checkbox, was missing a for="" attribute'
81
+ end
82
+ end
83
+
84
+ def check_input_type_is_radio
85
+ if @input[:type] == 'radio'
86
+ raise "Found the label, but it is associated with an input with type=\"#{@input[:type]}\" not a checkbox"
87
+ end
88
+ end
89
+
90
+ def check_label_classes
91
+ label_classes = @label[:class].to_s.split(/\s+/)
92
+
93
+ if !label_classes.include?('govuk-label') && !label_classes.include?('govuk-checkboxes__label')
94
+ raise "Found label but it is missing the govuk-label and govuk-checkboxes__label classes"
95
+ elsif !label_classes.include?('govuk-label')
96
+ raise "Found label but it is missing the govuk-label class"
97
+ elsif !label_classes.include?('govuk-checkboxes__label')
98
+ raise "Found label but it is missing the govuk-checkboxes__label class"
99
+ end
100
+ end
101
+
102
+ def check_input_class
103
+ input_classes = @input[:class].to_s.split(/\s+/)
104
+
105
+ if !input_classes.include?('govuk-checkboxes__input')
106
+ raise "Found checkbox but it is missing the govuk-checkboxes__input class"
107
+ end
108
+ end
109
+
110
+
111
+ def check_for_hint
112
+ if @hint.nil? || @hint.text != hint_text
113
+ other_hints = @label.ancestor('.govuk-checkboxes__item').all('.govuk-hint')
114
+
115
+ if other_hints.size > 0 && hint_text
116
+ raise "Found checkbox but could not find matching hint. Found the hint \"#{other_hints.first.text}\" instead"
117
+ end
118
+ end
119
+ end
120
+
121
+ def check_that_hint_is_associated_with_input
122
+ hint_id = @hint[:id]
123
+
124
+ if hint_id.to_s.strip == ""
125
+ if hint_text
126
+ raise "Found checkbox and hint, but the hint is not associated with the input using aria. And an ID to the hint and Add aria-describedby= to the input with that ID."
127
+ else
128
+ raise "Found checkbox, but also found a hint that is not associated with the input using aria. And an ID to the hint and Add aria-describedby= to the input with that ID."
129
+ end
130
+ end
131
+
132
+ if !@input["aria-describedby"].to_s.split(/\s+/).include?(hint_id)
133
+ if hint_text
134
+ raise "Found checkbox and hint, but the hint is not associated with the input using aria. Add aria-describedby=\"#{hint_id}\" to the input."
135
+ else
136
+ raise "Found checkbox, but also found a hint that is not associated with the input using aria. Add aria-describedby=\"#{hint_id}\" to the input."
137
+ end
138
+ end
139
+ end
140
+
141
+ def check_that_checkbox_not_checked
142
+ if @input[:checked]
143
+ raise "Found checkbox, but it was already checked"
144
+ end
145
+ end
146
+
147
+ def check_that_checkbox_is_checked
148
+ if !@input[:checked]
149
+ raise "Found checkbox, but it was already unchecked"
150
+ end
151
+ end
152
+
153
+ end
154
+
155
+ def check_govuk_checkbox(label_text, hint: nil)
156
+ GovukCheckbox.new(page: page, label_text: label_text, hint_text: hint).check
157
+ end
158
+
159
+ def uncheck_govuk_checkbox(label_text, hint: nil)
160
+ GovukCheckbox.new(page: page, label_text: label_text, hint_text: hint).uncheck
161
+ end
162
+
163
+ RSpec.configure do |rspec|
164
+ rspec.include self
165
+ end
166
+
167
+ end
@@ -0,0 +1,104 @@
1
+ module GovukRSpecHelpers
2
+ class ChooseGovukRadio
3
+
4
+ attr_reader :page, :label_text, :hint_text
5
+
6
+ def initialize(page:, label_text:, hint_text:)
7
+ @label_text = label_text
8
+ @hint_text = hint_text
9
+ @page = page
10
+ end
11
+
12
+ def choose
13
+ labels = page.all('label', text: label_text, exact_text: true, normalize_ws: true)
14
+
15
+ if labels.size == 0
16
+ check_for_input_id_match
17
+
18
+ raise "Unable to find label with the text \"#{label_text}\""
19
+ end
20
+
21
+ @label = labels.first
22
+
23
+ inputs_matching_label = page.all(id: @label[:for])
24
+
25
+ if inputs_matching_label.size == 1
26
+ @input = inputs_matching_label.first
27
+ end
28
+
29
+ check_that_fieldset_legend_was_specified
30
+
31
+ if hint_text
32
+ check_for_hint
33
+ check_that_hint_is_associated_with_input
34
+ end
35
+
36
+
37
+ @label.click
38
+ end
39
+
40
+ private
41
+
42
+ def check_for_input_id_match
43
+ inputs = page.all('input', id: label_text)
44
+
45
+ if inputs.size > 0
46
+
47
+ matching_labels = page.all("label[for=#{label_text}]")
48
+
49
+ if matching_labels.size > 0
50
+ raise "Use the full label text \"#{matching_labels.first.text}\" instead of the input ID"
51
+ end
52
+ end
53
+ end
54
+
55
+ def check_that_fieldset_legend_was_specified
56
+ if page.current_scope.is_a?(Capybara::Node::Document)
57
+
58
+ fieldset = @label.ancestor('fieldset')
59
+ legend = fieldset.find('legend')
60
+
61
+ if legend
62
+ raise "Specify the legend using: within_govuk_fieldset \"#{legend.text}\" do"
63
+ end
64
+ end
65
+ end
66
+
67
+ def check_for_hint
68
+ radio_item = @label.ancestor('.govuk-radios__item')
69
+
70
+ hints = radio_item.all('.govuk-hint', text: hint_text, exact_text: true, normalize_ws: true)
71
+
72
+ if hints.size == 0
73
+ other_hints = radio_item.all('.govuk-hint')
74
+
75
+ if other_hints.size > 0
76
+ raise "Found radio but could not find matching hint. Found the hint \"#{other_hints.first.text}\" instead"
77
+ end
78
+ end
79
+
80
+ @hint = hints.first
81
+ end
82
+
83
+ def check_that_hint_is_associated_with_input
84
+ hint_id = @hint[:id]
85
+
86
+ if hint_id.to_s.strip == ""
87
+ raise "Found radio and hint, but the hint is not associated with the input using aria. And an ID to the hint and Add aria-describedby= to the input with that ID."
88
+ end
89
+
90
+ if !@input["aria-describedby"].to_s.split(/\s+/).include?(hint_id)
91
+ raise "Found radio and hint, but the hint is not associated with the input using aria. Add aria-describedby=#{hint_id} to the input."
92
+ end
93
+ end
94
+ end
95
+
96
+ def choose_govuk_radio(label_text, hint: nil)
97
+ ChooseGovukRadio.new(page: page, label_text: label_text, hint_text: hint).choose
98
+ end
99
+
100
+ RSpec.configure do |rspec|
101
+ rspec.include self
102
+ end
103
+
104
+ end
@@ -0,0 +1,109 @@
1
+ module GovukRSpecHelpers
2
+ class ClickButton
3
+
4
+ attr_reader :page, :button_text, :disabled
5
+
6
+ def initialize(page:, button_text:, disabled: false)
7
+ @page = page
8
+ @button_text = button_text
9
+ @disabled = disabled
10
+ end
11
+
12
+ def click
13
+ @buttons = page.all('button', text: button_text, exact_text: true)
14
+
15
+ if @buttons.empty?
16
+ @buttons = page.all('a.govuk-button', text: button_text, exact_text: true)
17
+ end
18
+
19
+ if @buttons.empty?
20
+ @buttons = page.all("input[type=submit][value=\"#{Capybara::Selector::CSS.escape(button_text)}\"]")
21
+ end
22
+
23
+ if @buttons.size == 0
24
+ check_for_inexact_match
25
+ raise "Unable to find button \"#{button_text}\""
26
+ end
27
+
28
+ check_that_button_text_is_unique_on_the_page
29
+
30
+ @button = @buttons.first
31
+
32
+ check_data_module_attribute_is_present
33
+ check_role_is_present_if_button_is_a_link
34
+ check_button_is_not_draggable_if_button_is_a_link
35
+ check_if_button_is_disabled
36
+ check_for_govuk_class
37
+
38
+ @button.click unless disabled
39
+ end
40
+
41
+ private
42
+
43
+ def check_for_inexact_match
44
+ buttons_without_exact_match = page.all('button', text: button_text)
45
+
46
+ if buttons_without_exact_match.size > 0
47
+ raise "Unable to find button \"#{button_text}\" but did find button with the text \"#{buttons_without_exact_match.first.text}\" - include the full button text including any visually-hidden text"
48
+ end
49
+ end
50
+
51
+ def check_that_button_text_is_unique_on_the_page
52
+ if @buttons.size > 1
53
+ raise "There are #{@buttons.size} buttons with the text \"#{button_text}\" - buttons should be unique within a page"
54
+ end
55
+ end
56
+
57
+ def check_data_module_attribute_is_present
58
+ if @button["data-module"] != "govuk-button"
59
+ raise "Button is missing the data-module=\"govuk-button\" attribute"
60
+ end
61
+ end
62
+
63
+ def check_role_is_present_if_button_is_a_link
64
+ if @button.tag_name == 'a' && @button["role"] != "button"
65
+ raise "Button found, but `role=\"button\"` is missing, this is needed on links styled as buttons"
66
+ end
67
+ end
68
+
69
+ def check_button_is_not_draggable_if_button_is_a_link
70
+ if @button.tag_name == 'a' && @button["draggable"] != "false"
71
+ raise "Button found, but `draggable=\"false\"` is missing, this is needed on links styled as buttons"
72
+ end
73
+ end
74
+
75
+ def check_if_button_is_disabled
76
+ button_classes = @button[:class].to_s.split(/\s/).collect(&:strip)
77
+
78
+ if @button["disabled"] == "disabled" && !disabled
79
+ raise "Button is disabled. Avoid using disabled buttons as they have poor contrast and can confuse users. If this is unavoidable, use click_govuk_button(\"#{button_text}\", disabled: true)"
80
+ end
81
+
82
+ if disabled && !button_classes.include?('govuk-button--disabled')
83
+ raise "Disabled button is missing the govuk-button--disabled class"
84
+ end
85
+
86
+ if disabled && @button["aria-disabled"].to_s != "true"
87
+ raise 'Disabled button is missing aria-disabled="true"'
88
+ end
89
+ end
90
+
91
+ def check_for_govuk_class
92
+ button_classes = @button[:class].to_s.split(/\s/).collect(&:strip)
93
+ if button_classes.empty?
94
+ raise "Button is missing a class, should contain \"govuk-button\""
95
+ elsif !button_classes.include?('govuk-button')
96
+ raise "Button is missing the govuk-button class, contains #{button_classes.join(', ')}"
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+ def click_govuk_button(button_text, disabled: false)
103
+ ClickButton.new(page: page, button_text: button_text, disabled: disabled).click
104
+ end
105
+
106
+ RSpec.configure do |rspec|
107
+ rspec.include self
108
+ end
109
+ end
@@ -0,0 +1,110 @@
1
+ module GovukRSpecHelpers
2
+ class ClickLink
3
+
4
+ attr_reader :link_text, :page
5
+
6
+ VALID_LINK_CLASSES = ["govuk-link", "govuk-breadcrumbs__link", "govuk-back-link", "govuk-header__link", "govuk-footer__link", "govuk-notification-banner__link", "govuk-skip-link", "govuk-tabs__tab"]
7
+
8
+ AMIBIGOUS_LINK_TEXTS = ["Change", "Add", "Remove"]
9
+
10
+ def initialize(page:, link_text:)
11
+ @page = page
12
+ @link_text = link_text
13
+ end
14
+
15
+ def click
16
+ @links = page.all('a', text: link_text, exact_text: true, normalize_ws: true)
17
+
18
+ if @links.size == 0
19
+ check_whether_there_is_an_inexact_match
20
+ check_whether_there_is_an_href_match
21
+ check_whether_there_is_an_id_match
22
+
23
+ raise "Unable to find link \"#{link_text}\""
24
+ end
25
+
26
+ check_link_text_is_unique
27
+ check_link_text_is_not_ambiguous
28
+
29
+ @link = @links.first
30
+ @link_classes = @link[:class].to_s.split(/\s/).collect(&:strip)
31
+
32
+ check_link_is_not_styled_as_button
33
+ check_link_warns_if_opening_in_a_new_tab
34
+ check_link_does_not_contain_a_button
35
+ check_link_has_a_valid_class
36
+
37
+ @link.click
38
+ end
39
+
40
+ private
41
+
42
+ def check_whether_there_is_an_inexact_match
43
+ links_without_exact_match = page.all('a', text: link_text)
44
+ if page.all('a', text: link_text).size > 0
45
+ raise "Unable to find link \"#{link_text}\" but did find link with the text \"#{links_without_exact_match.first.text}\" - include the full link text including any visually-hidden text"
46
+ end
47
+ end
48
+
49
+ def check_whether_there_is_an_href_match
50
+ links_with_href_match = page.all(:link, href: link_text)
51
+ if links_with_href_match.size > 0
52
+ raise "Use the full link text within click_govuk_link() instead of the link href"
53
+ end
54
+ end
55
+
56
+ def check_whether_there_is_an_id_match
57
+ links_with_id_match = page.all(:link, link_text)
58
+ if links_with_id_match.size > 0
59
+ raise "Use the full link text within click_govuk_link() instead of the link id"
60
+ end
61
+ end
62
+
63
+ def check_link_text_is_not_ambiguous
64
+ if AMIBIGOUS_LINK_TEXTS.include?(link_text.strip)
65
+ raise "The link was found, but the text \"#{link_text}\" is ambiguous if heard out of context - add some visually-hidden text"
66
+ end
67
+ end
68
+
69
+ def check_link_text_is_unique
70
+ if @links.size > 1
71
+ raise "There are #{@links.size} links with the link text \"#{link_text}\" - links should be unique within a page"
72
+ end
73
+ end
74
+
75
+ def check_link_is_not_styled_as_button
76
+ if @link_classes.include?('govuk-button')
77
+ raise "The link was found, but is styled as a button. Use `click_govuk_button` instead."
78
+ end
79
+ end
80
+
81
+ def check_link_warns_if_opening_in_a_new_tab
82
+ if @link[:target] == "_blank" && !link_text.include?("opens in new tab")
83
+ raise "The link was found, but is set to open in a new tab. Either remove this, or add \"(opens in new tab)\" to the link text"
84
+ end
85
+ end
86
+
87
+ def check_link_does_not_contain_a_button
88
+ if @link.all('button').any?
89
+ raise "The link was found, but it contains a button – use either a link or button but not both"
90
+ end
91
+ end
92
+
93
+ def check_link_has_a_valid_class
94
+ if @link_classes.empty?
95
+ raise "\"#{link_text}\" link is missing a class, should contain \"govuk-link\""
96
+ elsif !@link_classes.any? {|link_class| VALID_LINK_CLASSES.include?(link_class) }
97
+ raise "\"#{link_text}\" link is missing a govuk-link class, contains #{@link_classes.join(', ')}"
98
+ end
99
+ end
100
+ end
101
+
102
+ def click_govuk_link(link_text)
103
+ ClickLink.new(page: page, link_text: link_text).click
104
+ end
105
+
106
+ RSpec.configure do |rspec|
107
+ rspec.include self
108
+ end
109
+
110
+ end
@@ -0,0 +1,150 @@
1
+ module GovukRSpecHelpers
2
+ class FillInGovUKTextField
3
+
4
+ attr_reader :page, :label, :hint, :with
5
+
6
+ def initialize(page:, label:, hint:, with:)
7
+ @page = page
8
+ @label = label
9
+ @hint = hint
10
+ @with = with
11
+ end
12
+
13
+ def fill_in
14
+ labels = page.all('label', text: label, exact_text: true, normalize_ws: true)
15
+
16
+ if labels.size == 0
17
+ check_for_inexact_label_match
18
+ check_for_field_name_match
19
+
20
+ raise "Unable to find label with the text #{label}"
21
+ end
22
+
23
+ @label = labels.first
24
+
25
+ check_label_has_a_for_attribute
26
+
27
+ label_for = @label[:for]
28
+ @inputs = page.all(id: label_for)
29
+
30
+ check_label_is_associated_with_a_field
31
+ check_there_is_only_1_element_with_the_associated_id
32
+
33
+ @input = @inputs.first
34
+
35
+ check_associated_element_is_a_form_field
36
+ check_input_type_is_text
37
+
38
+ aria_described_by_ids = @input["aria-describedby"].to_s.strip.split(/\s+/)
39
+
40
+ @described_by_elements = []
41
+
42
+ if aria_described_by_ids.size > 0
43
+ aria_described_by_ids.each do |aria_described_by_id|
44
+
45
+ check_there_is_only_one_element_with_id(aria_described_by_id)
46
+ @described_by_elements << page.find(id: aria_described_by_id)
47
+
48
+ end
49
+ end
50
+
51
+ if hint
52
+ check_field_is_described_by_a_hint
53
+ check_hint_matches_text_given
54
+ end
55
+
56
+ @input.set(with)
57
+ end
58
+
59
+ private
60
+
61
+ def check_for_inexact_label_match
62
+ labels_not_using_exact_match = page.all('label', text: label)
63
+ if labels_not_using_exact_match.size > 0
64
+ 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"
65
+ end
66
+ end
67
+
68
+ def check_for_field_name_match
69
+ inputs_matching_name = page.all("input[name=\"#{label}\"]")
70
+
71
+ if inputs_matching_name.size > 0
72
+
73
+ input_matching_name = inputs_matching_name.first
74
+
75
+ labels = page.all("label[for=\"#{input_matching_name['id']}\"]")
76
+
77
+ if labels.size > 0
78
+ raise "Use the full label text \"#{labels.first.text}\" instead of the field name"
79
+ end
80
+ end
81
+ end
82
+
83
+ def check_label_has_a_for_attribute
84
+ if @label[:for].to_s.strip == ""
85
+ raise "Found the label but it is missing a \"for\" attribute to associate it with an input"
86
+ end
87
+ end
88
+
89
+ def check_label_is_associated_with_a_field
90
+ if @inputs.size == 0
91
+ raise "Found the label but there is no field with the ID \"#{@label[:for]}\" which matches the label‘s \"for\" attribute"
92
+ end
93
+ end
94
+
95
+ def check_there_is_only_1_element_with_the_associated_id
96
+ if @inputs.size > 1
97
+ raise "Found the label but there there are #{@inputs.size} elements with the ID \"#{@label[:for]}\" which matches the label‘s \"for\" attribute"
98
+ end
99
+ end
100
+
101
+ def check_associated_element_is_a_form_field
102
+ if !['input', 'textarea', 'select'].include?(@input.tag_name)
103
+ raise "Found the label but but it is associated with a <#{@input.tag_name}> element instead of a form field"
104
+ end
105
+ end
106
+
107
+ def check_input_type_is_text
108
+ raise "Found the field, but it has type=\"#{@input[:type]}\", expected type=\"text\"" unless @input[:type] == "text"
109
+ end
110
+
111
+ def check_field_is_described_by_a_hint
112
+ if @described_by_elements.size == 0
113
+ check_if_the_hint_exists_but_is_not_associated_with_field
114
+
115
+ raise "Found the field but could not find the hint \"#{hint}\""
116
+ end
117
+ end
118
+
119
+ def check_if_the_hint_exists_but_is_not_associated_with_field
120
+ if page.all('.govuk-hint', text: hint).size > 0
121
+ raise "Found the field and the hint, but not field is not associated with the hint using aria-describedby"
122
+ end
123
+ end
124
+
125
+ def check_hint_matches_text_given
126
+ hint_matching_id = @described_by_elements.find {|element| element[:class].include?("govuk-hint") }
127
+ if hint_matching_id.text != hint
128
+ raise "Found the label but the associated hint is \"#{hint_matching_id.text}\" not \"#{hint}\""
129
+ end
130
+ end
131
+
132
+ def check_there_is_only_one_element_with_id(aria_described_by_id)
133
+ elements_matching_id = page.all(id: aria_described_by_id)
134
+ if elements_matching_id.size == 0
135
+ raise "Found the field but it has an \"aria-describedby=#{aria_described_by_id}\" attribute and no hint with that ID exists"
136
+ elsif elements_matching_id.size > 1
137
+ raise "Found the field but it has an \"aria-describedby=#{aria_described_by_id}\" attribute and 2 elements with that ID exist"
138
+ end
139
+ end
140
+
141
+ end
142
+
143
+ def fill_in_govuk_text_field(label, hint: nil, with:)
144
+ FillInGovUKTextField.new(page:, label:, hint:, with:).fill_in
145
+ end
146
+
147
+ RSpec.configure do |rspec|
148
+ rspec.include self
149
+ end
150
+ end
@@ -0,0 +1,13 @@
1
+
2
+ require 'rspec'
3
+ require 'capybara'
4
+
5
+ require_relative 'summarise_errors_matcher'
6
+ require_relative 'summarise_matcher'
7
+
8
+ require_relative 'check_govuk_checkbox'
9
+ require_relative 'click_govuk_link'
10
+ require_relative 'click_govuk_button'
11
+ require_relative 'fill_in_govuk_text_field'
12
+ require_relative 'choose_govuk_radio'
13
+ require_relative 'within_govuk_fieldset'
@@ -0,0 +1,119 @@
1
+ module GovukRSpecHelpers
2
+ module SummariseErrorsMatcher
3
+
4
+ extend RSpec::Matchers::DSL
5
+
6
+ define :summarise_errors do |expected|
7
+ match do |_actual|
8
+ title_contains_prefix &&
9
+ error_summary_title &&
10
+ expected.all? do |expected_error|
11
+ error_messages && error_messages[expected.index(expected_error)] == expected_error
12
+ end && expected.size == error_messages.size &&
13
+ all_error_messages_contain_links &&
14
+ all_error_messages_links_are_valid
15
+ end
16
+
17
+ failure_message do |actual|
18
+ missing_error = expected.find {|expected_error| !error_messages.include?(expected_error) }
19
+ if !title
20
+ "Missing <title> tag"
21
+ elsif !title_contains_prefix
22
+ "Title tag is missing the error prefix: ‘#{title}’"
23
+ elsif !error_summary_title
24
+ "Missing an error summary title"
25
+ elsif missing_error
26
+ "Missing error message: ‘#{missing_error}’"
27
+ elsif !all_error_messages_contain_links
28
+ "Error message ‘#{error_message_missing_link}’ isn’t linked to anything"
29
+ elsif !all_error_messages_links_are_valid
30
+ "Error message ‘#{error_message_with_invalid_link.text(normalize_ws: true)}’ links to ##{error_message_with_invalid_link[:href].split('#').last} but no input field has this ID or name"
31
+ elsif expected.size == error_messages.size
32
+ "Error messages appear in a different order"
33
+ elsif expected.size < error_messages.size
34
+ "An extra error message is present"
35
+ else
36
+ "Unexpected error"
37
+ end
38
+ end
39
+
40
+ def html
41
+ @html ||= Capybara::Node::Simple.new(actual.is_a?(String) ? actual : actual.html)
42
+ end
43
+
44
+ def title
45
+ @title ||= html.title
46
+ end
47
+
48
+ def title_contains_prefix
49
+ title.to_s.start_with?("Error: ")
50
+ end
51
+
52
+ def all_error_messages_contain_links
53
+ error_message_items.all? do |error_message_item|
54
+ error_message_item.all(:link).first
55
+ end
56
+ end
57
+
58
+ def all_error_messages_links_are_valid
59
+ error_message_items.all? do |error_message_item|
60
+ link = error_message_item.all(:link).first
61
+
62
+ if link
63
+ link_fragment = link[:href].split('#').last
64
+
65
+ link_target= html.all(id: link_fragment).first || html.all(:field, name: link_fragment).first
66
+
67
+ link_target
68
+ else
69
+ false
70
+ end
71
+ end
72
+ end
73
+
74
+ def error_message_with_invalid_link
75
+ invalid_link = error_message_links.find do |error_message_link|
76
+ link_fragment = error_message_link[:href].split('#').last
77
+
78
+ link_target= html.all(id: link_fragment).first || html.all(:field, name: link_fragment).first
79
+
80
+ !link_target
81
+ end
82
+ end
83
+
84
+ def error_message_missing_link
85
+ error_message_items.find do |error_message_item|
86
+ !error_message_item.all(:link).first
87
+ end.text(normalize_ws: true)
88
+ end
89
+
90
+ def error_messages
91
+ @error_messages ||= (error_summary_list && error_summary_list.all('li').collect {|li| li.text(normalize_ws: true) } || [])
92
+ end
93
+
94
+ def error_message_links
95
+ @error_message_links ||= error_message_items.collect {|item| item.all(:link).first }
96
+ end
97
+
98
+ def error_message_items
99
+ @error_message_items ||= (error_summary_list && error_summary_list.all('li') || [])
100
+ end
101
+
102
+ def error_summary_list
103
+ @error_summary_list ||= (error_summary && error_summary.all('.govuk-error-summary__list').first)
104
+ end
105
+
106
+ def error_summary_title
107
+ @error_summary_title ||= error_summary.all('h2.govuk-error-summary__title').first
108
+ end
109
+
110
+ def error_summary
111
+ @error_summary ||= html.all('.govuk-error-summary').first
112
+ end
113
+ end
114
+
115
+ RSpec.configure do |rspec|
116
+ rspec.include self
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,71 @@
1
+ module GovukRSpecHelpers
2
+ module SummariseMatcher
3
+
4
+ extend RSpec::Matchers::DSL
5
+
6
+ define :summarise do |expected|
7
+ match do |_actual|
8
+ if expected[:action]
9
+ key_html && value_html && value_match? && action_link && action_link[:href] == expected[:action][:href]
10
+ else
11
+ key_html && value_html && value_match?
12
+ end
13
+ end
14
+
15
+ failure_message do |actual|
16
+ if !key_html
17
+ "Could not find the key ‘#{expected[:key]}’ within\n\n #{actual}"
18
+ elsif !value_html
19
+ "Could not find a <dd class=\"govuk-summary-list__value\"> element within HTML: \n#{row_html.native.to_html}"
20
+ elsif value_text != expected[:value]
21
+ "Expected ‘#{expected[:key]}’ value to be ‘#{expected[:value]}’ but was ‘#{value_text}’"
22
+ elsif !action_link
23
+ "Could not find the link ‘#{expected[:action][:text]}’ within HTML: \n#{actions_html.native.to_html}"
24
+ else
25
+ "Expected link href to be #{expected[:action][:href]}, was #{action_link[:href]}"
26
+ end
27
+ end
28
+
29
+ def html
30
+ @html ||= Capybara::Node::Simple.new(actual.is_a?(String) ? actual : actual.html)
31
+ end
32
+
33
+ def key_html
34
+ @key_html ||= html.all('dt.govuk-summary-list__key', exact_text: expected[:key], normalize_ws: true).first
35
+ end
36
+
37
+ def row_html
38
+ @row_html ||= key_html.ancestor('.govuk-summary-list__row')
39
+ end
40
+
41
+ def actions_html
42
+ @actions_html ||= row_html.all('dd.govuk-summary-list__actions').first
43
+ end
44
+
45
+ def value_html
46
+ @value_html ||= row_html.all('dd.govuk-summary-list__value').first
47
+ end
48
+
49
+ def value_text
50
+ @value_text ||= value_html.text(normalize_ws: true)
51
+ end
52
+
53
+ def action_link
54
+ @action_link ||= actions_html.all(:link, exact_text: expected[:action][:text], normalize_ws: true).first
55
+ end
56
+
57
+ def value_match?
58
+ if expected[:value].is_a?(Regexp)
59
+ value_text.match?(expected[:value])
60
+ else
61
+ value_text == expected[:value]
62
+ end
63
+ end
64
+ end
65
+
66
+ RSpec.configure do |rspec|
67
+ rspec.include self
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,74 @@
1
+ module GovukRSpecHelpers
2
+ class WithinGovukFieldset
3
+
4
+ attr_reader :page, :legend_text, :hint, :block
5
+
6
+ def initialize(page:, legend_text:, hint:, block:)
7
+ @legend_text = legend_text
8
+ @hint = hint
9
+ @page = page
10
+ @block = block
11
+ end
12
+
13
+ def within
14
+
15
+ legends = page.all('legend', text: legend_text, exact_text: true, normalize_ws: true)
16
+
17
+ if legends.size == 0
18
+ check_for_fieldset_id_match
19
+ raise "No fieldset found with matching legend"
20
+ end
21
+
22
+ legend = legends.first
23
+ @fieldset = legend.ancestor('fieldset')
24
+
25
+ check_hint
26
+
27
+ @page.within(@fieldset) do
28
+ block.call
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def check_hint
35
+ if hint
36
+ hint_elements = @fieldset.all('.govuk-hint', text: hint, exact_text: true, normalize_ws: true)
37
+
38
+ if hint_elements.size == 0
39
+ raise "Count not find hint with that text"
40
+ end
41
+
42
+ hint_id = hint_elements.first[:id]
43
+
44
+ if !@fieldset["aria-describedby"].to_s.split(/\s+/).include?(hint_id)
45
+ raise "Found hint but it is not associated with the fieldset using aria-describedby"
46
+ end
47
+
48
+ end
49
+ end
50
+
51
+ def check_for_fieldset_id_match
52
+ matching_fieldsets = page.all("fieldset[id=#{Capybara::Selector::CSS.escape(legend_text)}]")
53
+
54
+ if matching_fieldsets.size > 0
55
+
56
+ legends = matching_fieldsets.first.all('legend')
57
+
58
+ if legends.size > 0
59
+ raise "Use the full legend text \"#{legends.first.text}\" instead of the fieldset ID"
60
+ end
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ def within_govuk_fieldset(legend_text, hint: nil, &block)
67
+ WithinGovukFieldset.new(page: page, legend_text: legend_text, hint: hint, block: block).within
68
+ end
69
+
70
+ RSpec.configure do |rspec|
71
+ rspec.include self
72
+ end
73
+
74
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: govuk-rspec-helpers
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Frankie Roberto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-10-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: capybara
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.24'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.24'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-expectations
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: ''
56
+ email:
57
+ - frankie@frankieroberto.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE.txt
63
+ - README.md
64
+ - Rakefile
65
+ - lib/check_govuk_checkbox.rb
66
+ - lib/choose_govuk_radio.rb
67
+ - lib/click_govuk_button.rb
68
+ - lib/click_govuk_link.rb
69
+ - lib/fill_in_govuk_text_field.rb
70
+ - lib/govuk_rspec_helpers.rb
71
+ - lib/summarise_errors_matcher.rb
72
+ - lib/summarise_matcher.rb
73
+ - lib/within_govuk_fieldset.rb
74
+ homepage: https://x-govuk.github.io
75
+ licenses:
76
+ - MIT
77
+ metadata:
78
+ homepage_uri: https://x-govuk.github.io
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 3.1.4
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.4.10
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: RSpec test helpers for GOV.UK services
98
+ test_files: []