govuk-rspec-helpers 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +7 -0
- data/Rakefile +8 -0
- data/lib/check_govuk_checkbox.rb +167 -0
- data/lib/choose_govuk_radio.rb +104 -0
- data/lib/click_govuk_button.rb +109 -0
- data/lib/click_govuk_link.rb +110 -0
- data/lib/fill_in_govuk_text_field.rb +150 -0
- data/lib/govuk_rspec_helpers.rb +13 -0
- data/lib/summarise_errors_matcher.rb +119 -0
- data/lib/summarise_matcher.rb +71 -0
- data/lib/within_govuk_fieldset.rb +74 -0
- metadata +98 -0
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,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: []
|