spec/govuk_design_system_formbuilder/builder/error_summary_spec.rb
describe GOVUKDesignSystemFormBuilder::FormBuilder do
include_context 'setup builder'
include_context 'setup examples'
let(:method) { :govuk_error_summary }
describe '#govuk_error_summary' do
let(:args) { [method] }
subject { builder.send(*args) }
context 'when the object has errors' do
before { object.valid? }
include_examples 'HTML formatting checks'
specify 'the error summary should be present' do
expect(subject).to have_tag('div', with: { class: 'govuk-error-summary' })
end
specify 'the error summary should have a title' do
expect(subject).to have_tag(
'h2',
with: { class: 'govuk-error-summary__title' }
)
end
specify 'the error summary should have the correct accessibility attributes' do
expected_attributes = { class: 'govuk-error-summary', 'data-module' => 'govuk-error-summary' }
expect(subject).to have_tag('div', with: expected_attributes) do
with_tag('div', with: { role: 'alert' }) do
with_tag('ul', with: { class: 'govuk-error-summary__list' })
end
end
end
describe 'error messages' do
let(:kwargs) { {} }
subject! { builder.send(*args, **kwargs) }
context 'when there are multiple errors each with one error message' do
let(:object) { Person.new(favourite_colour: nil, projects: []) }
specify 'the error summary should contain a list with one error message per field' do
expect(subject).to have_tag('ul', with: { class: %w(govuk-list govuk-error-summary__list) }) do
expect(subject).to have_tag('li', text: 'Choose a favourite colour')
expect(subject).to have_tag('li', text: 'Select at least one project')
end
end
end
context 'when there are multiple errors and one has multiple error messages' do
let(:object) { Person.new(name: nil, favourite_colour: nil) }
specify 'the error summary should contain a list with one error message per field' do
expect(subject).to have_tag('ul', with: { class: %w(govuk-list govuk-error-summary__list) }) do
expect(subject).to have_tag('li', text: 'Choose a favourite colour')
expect(subject).to have_tag('li', text: 'Enter a name')
end
end
end
specify 'the error message list should contain the correct messages' do
object.errors.messages.each_value do |message_list|
expect(subject).to have_tag('li', text: message_list.first) do
end
end
end
specify 'the error message list should contain links to relevant errors' do
object.errors.messages.each_key do |attribute|
expect(subject).to have_tag('a', with: {
href: "#person-#{underscores_to_dashes(attribute)}-field-error",
'data-turbo' => false
})
end
end
describe 'linking to elements' do
it_behaves_like 'an error summary linking directly to a form element', :govuk_text_field
it_behaves_like 'an error summary linking directly to a form element', :govuk_number_field
it_behaves_like 'an error summary linking directly to a form element', :govuk_phone_field
it_behaves_like 'an error summary linking directly to a form element', :govuk_url_field
it_behaves_like 'an error summary linking directly to a form element', :govuk_email_field
it_behaves_like 'an error summary linking directly to a form element', :govuk_file_field
it_behaves_like 'an error summary linking directly to a form element', :govuk_text_area, 'textarea'
describe 'collection select boxes' do
let(:object) { Person.new(favourite_colour: nil) }
let(:identifier) { 'person-favourite-colour-field-error' }
subject do
builder.safe_join(
[
builder.govuk_error_summary,
builder.govuk_collection_select(:favourite_colour, colours, :id, :name)
]
)
end
specify "the error message should link directly to the govuk_collection_select field" do
expect(subject).to have_tag('a', with: { href: "#" + identifier })
expect(subject).to have_tag('select', with: { id: identifier })
end
end
describe 'radio button collections' do
let(:object) { Person.new(favourite_colour: nil) }
let(:identifier) { 'person-favourite-colour-field-error' }
subject do
builder.content_tag('div') do
builder.safe_join(
[
builder.govuk_error_summary,
builder.govuk_collection_radio_buttons(:favourite_colour, colours, :id, :name)
]
)
end
end
specify 'the error message should link to only one radio button' do
expect(subject).to have_tag('a', with: { href: "#" + identifier })
expect(subject).to have_tag('input', with: { type: 'radio', id: identifier }, count: 1)
end
specify 'the radio button linked to should be first' do
first_radio = parsed_subject.css('input').find { |e| e['type'] == 'radio' }
expect(first_radio['id']).to eql(identifier)
end
specify 'there should be a label associated with the error link target' do
first_label = parsed_subject.css('label').first
expect(first_label['for']).to eql(identifier)
end
end
describe 'radio button fieldsets' do
let(:object) { Person.new(favourite_colour: nil) }
let(:identifier) { 'person-favourite-colour-field-error' }
let(:second_radio_identifier) { 'person-favourite-colour-green-field' }
subject do
builder.tag.div do
builder.safe_join(
[
builder.govuk_error_summary,
builder.govuk_radio_buttons_fieldset(:favourite_colour) do
builder.safe_join(
[
builder.govuk_radio_button(:favourite_colour, :red, label: { text: red_label }, link_errors: true),
builder.govuk_radio_button(:favourite_colour, :green, label: { text: green_label })
]
)
end
]
)
end
end
specify 'the error message should link to only one radio button' do
expect(subject).to have_tag('a', with: { href: "#" + identifier })
expect(subject).to have_tag('input', with: { type: 'radio', id: identifier }, count: 1)
end
specify 'the radio button linked to should be first' do
first_radio = parsed_subject.css('input').find { |e| e['type'] == 'radio' }
expect(first_radio['id']).to eql(identifier)
end
specify 'there should be a label associated with the error link target' do
first_label = parsed_subject.css('label').first
expect(first_label['for']).to eql(identifier)
end
specify 'the second radio button should have a regular id' do
expect(subject).to have_tag('input', with: { id: second_radio_identifier })
end
end
describe 'check box collections' do
let(:object) { Person.new(projects: nil) }
let(:identifier) { 'person-projects-field-error' }
subject do
builder.tag.div do
builder.safe_join(
[
builder.govuk_error_summary,
builder.govuk_collection_check_boxes(:projects, projects, :id, :name)
]
)
end
end
specify 'the error message should link to only one check box' do
expect(subject).to have_tag('a', with: { href: "#" + identifier })
expect(subject).to have_tag('input', with: { type: 'checkbox', id: identifier }, count: 1)
end
specify 'the check box linked to should be first' do
first_checkbox = parsed_subject.css('input').find { |e| e['type'] == 'checkbox' }
expect(first_checkbox['id']).to eql(identifier)
end
specify 'there should be a label associated with the error link target' do
first_label = parsed_subject.css('label').first
expect(first_label['for']).to eql(identifier)
end
end
describe 'check box fieldsets' do
let(:object) { Person.new }
let(:identifier) { 'person-projects-field-error' }
let(:second_checkbox_identifier) { %(person-projects-#{project_y.id}-field) }
subject do
builder.tag.div do
builder.safe_join(
[
builder.govuk_error_summary,
builder.govuk_check_boxes_fieldset(:projects) do
builder.safe_join(
[
builder.govuk_check_box(:projects, project_x.id, label: { text: project_x.name }, link_errors: true),
builder.govuk_check_box(:projects, project_y.id, label: { text: project_y.name })
]
)
end
]
)
end
end
specify 'the error message should link to only one check box' do
expect(subject).to have_tag('a', with: { href: "#" + identifier })
expect(subject).to have_tag('input', with: { type: 'checkbox', id: identifier }, count: 1)
end
specify 'the checkbox button linked to should be first' do
first_checkbox = parsed_subject.css('input').find { |e| e['type'] == 'checkbox' }
expect(first_checkbox['id']).to eql(identifier)
end
specify 'there should be a label associated with the error link target' do
first_label = parsed_subject.css('label').first
expect(first_label['for']).to eql(identifier)
end
specify 'the second radio button should have a regular id' do
expect(subject).to have_tag('input', with: { id: second_checkbox_identifier })
end
end
describe 'date fields' do
let(:object) { Person.new(born_on: Date.today.next_year(5)) }
let(:identifier) { 'person-born-on-field-error' }
# 1i is year, 2i is month, 3i is day - we're only dealing with day and month here
{ true => '2i', false => '3i' }.each do |omit_day, segment|
context "when omit_day is #{omit_day}" do
let(:first_date_segment_name) { "person[born_on(#{segment})]" }
subject do
builder.tag.div do
builder.safe_join(
[
builder.govuk_error_summary,
builder.govuk_date_field(:born_on, omit_day:)
]
)
end
end
specify 'the error message should link to only one check box' do
expect(subject).to have_tag('a', with: { href: "#" + identifier })
expect(subject).to have_tag('input', with: { id: identifier }, count: 1)
end
specify 'the targeted input should be the first non-hidden field' do
inputs = parsed_subject.css('input').reject { |el| el.attributes['type'].value == 'hidden' }
expect(inputs.first).to eql(parsed_subject.at_css('#' + identifier))
end
specify 'the targeted input should be the day field' do
expect(parsed_subject.at_css('#' + identifier).attribute('name').value).to eql(first_date_segment_name)
end
end
end
end
end
describe "custom sort order" do
let(:actual_order) { extract_field_names_from_errors_summary_list(parsed_subject) }
let(:overridden_order_symbols) { overridden_order.map(&:to_sym) }
context "by default" do
# the object here is Person, defined in spec/support/examples.rb
#
# the validation order is: name, favourite colour, projects, cv
#
# name is present on the object
specify "errors are displayed in the order they're defined in the model" do
expect(object.name).to be_present
expect(actual_order).to eql(%w(favourite_colour projects cv password))
end
end
describe "overriding" do
let(:object) { OrderedErrors.new }
let(:overridden_order) { %w(e d c b a) }
let(:kwargs) { { order: overridden_order_symbols } }
context "when all attributes are named in the ordering" do
# the default validation order is (:a, :b, :c, :d, :e)
#
# the overridden order is (:e, :d, :c, :b, :a)
specify "the error messages are displayed in the overridden order" do
expect(actual_order).to eql(overridden_order)
end
end
context "when there are attributes with errors that aren't named in the ordering" do
let(:object) { OrderedErrorsWithExtraAttributes.new }
# the default validation order is (:a, :b, :c, :d, :e)
#
# the overridden order is (:e, :d, :c, :b, :a)
#
# the extra attributes (:g, :h, :i) validation order is (:i, :h, :g)
specify "the errors for attributes with overridden ordering are first" do
expect(actual_order).to start_with(overridden_order)
end
specify "the errors for extra attributes appear last, in the order they were defined in the model" do
expect(actual_order).to end_with(%w(i h g))
end
end
context "when the ordering specifies attributes that aren't present on the object" do
let(:kwargs) { { order: overridden_order_symbols.append(%i(x y z)) } }
# there's no error_order method, ensure it doesn't blow up. it shouldn't
# because #index will return nil
specify "the error messages are displayed in the order they were defined in the model" do
expect(actual_order).to eql(overridden_order)
end
end
context "when there are more entries in the custom order than errors on the object" do
let(:object) { OrderedErrorsWithExtraAttributes.new }
let(:overridden_order_existing_attributes) { %w(a d c e b) }
let(:overridden_order) { ("m".."z").to_a.concat(overridden_order_existing_attributes) }
let(:kwargs) { { order: overridden_order_symbols } }
# the default validation order is (:a, :b, :c, :d, :e)
#
# the overridden order is :m..:z, :a, :d, :c, :e, :b)
#
# the counter used in ordering *should not* allow 'extra' attributes
# to come before ones specified in the override, such as in this
# contrived example where the index of our known fields are at the end
specify "the error messages are displayed in the order they were defined in the model" do
expect(actual_order).to start_with(overridden_order_existing_attributes)
end
end
end
end
end
end
context 'when the object has no errors' do
let(:object) { Person.valid_example }
subject { builder.send(method) }
specify 'no error summary should be present' do
expect(subject).to be_nil
end
end
context 'when the object does not support errors' do
let(:object) { Guest.example }
subject { builder.send(method) }
specify 'an error should be raised' do
expect { subject }.to raise_error(NoMethodError, /errors/)
end
end
describe 'when there are errors on the base object' do
let(:object) { Person.with_errors_on_base }
let(:error) { object.errors[:base].first }
context 'when an override is specified' do
let(:link_base_errors_to) { :name }
subject { builder.send(method, link_base_errors_to:) }
specify 'the override field should be linked to' do
expect(subject).to have_tag("a", text: error, with: { href: %(#person-#{link_base_errors_to}-field) })
end
end
context 'when no override is specified' do
subject { builder.send(method) }
specify 'the base field should be linked to' do
expect(subject).to have_tag("a", text: error, with: { href: %(#person-base-field-error) })
end
end
end
context 'extra attributes' do
before { object.valid? }
it_behaves_like 'a field that allows extra HTML attributes to be set' do
let(:described_element) { 'div' }
let(:expected_class) { 'govuk-error-summary' }
end
end
context 'when a block of html is supplied' do
let(:custom_content_tag) { :marquee }
let(:custom_content_text) { "Fix the things below" }
subject do
builder.send(*args) do
builder.content_tag(custom_content_tag, custom_content_text)
end
end
before { object.valid? }
specify "the custom content should be present in the error summary" do
expect(subject).to have_tag("div", with: { class: "govuk-error-summary" }) do
with_tag("div", with: { class: "govuk-error-summary__body" }) do
with_tag(custom_content_tag, text: custom_content_text)
end
end
end
end
context "when a custom presenter is supplied" do
before { object.valid? }
let(:custom_presenter) do
Class.new do
def initialize(error_messages)
@error_messages = error_messages
end
def formatted_error_messages
@error_messages.map { |attribute, messages| [attribute, messages.first.upcase] }
end
end
end
let(:expected_error_messages) do
object.errors.messages.each.with_object({}) { |(k, v), h| h[k.to_s] = v.first.upcase }
end
context "as a class that upcases the error messages" do
subject { builder.send(*args, presenter: custom_presenter) }
specify "uses the presenter to display error messages in the desired format" do
expect(subject).to have_tag("ul", with: { class: "govuk-error-summary__list" }) do
expected_error_messages.each do |attr, error_message|
with_tag("a", with: { href: underscores_to_dashes(%(#person-#{attr}-field-error)) }, text: error_message)
end
end
end
context "when the custom presenter is a class that doesn't implement #formatted_error_messages" do
let(:non_presenter) { OpenStruct }
subject { builder.send(*args, presenter: non_presenter) }
specify "fails with an appropriate error message" do
expect { subject }.to raise_error(ArgumentError, "error summary presenter doesn't implement #formatted_error_messages")
end
end
end
context "as an instance" do
subject { builder.send(*args, presenter: custom_presenter.new(object.errors.messages)) }
specify "uses the presenter to display error messages in the desired format" do
expect(subject).to have_tag("ul", with: { class: "govuk-error-summary__list" }) do
expected_error_messages.each do |attr, error_message|
with_tag("a", with: { href: underscores_to_dashes(%(#person-#{attr}-field-error)) }, text: error_message)
end
end
end
context "when the custom presenter is an instance that doesn't implement #formatted_error_messages" do
let(:non_presenter) { "totally not a presenter" }
subject { builder.send(*args, presenter: non_presenter) }
specify "fails with an appropriate error message" do
expect { subject }.to raise_error(ArgumentError, "error summary presenter doesn't implement #formatted_error_messages")
end
end
end
context "when the third item in the error messages array is present" do
let(:presenter_with_external_links) do
Class.new do
def initialize(error_messages)
@error_messages = error_messages
end
def formatted_error_messages
@error_messages.map { |attribute, messages| [attribute, messages.first, %(https://www.errors.com/#{attribute})] }
end
end
end
subject { builder.send(*args, presenter: presenter_with_external_links) }
specify "the third argument forms the hyperlink" do
object.errors.messages.transform_values(&:first).each do |attr, message|
expect(subject).to have_tag("a", with: { href: "https://www.errors.com/#{attr}" }, text: message)
end
end
end
end
end
end