govuk-rspec-helpers 0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []