DFE-Digital/govuk-formbuilder

View on GitHub
spec/govuk_design_system_formbuilder/builder/radios/collection_spec.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
describe GOVUKDesignSystemFormBuilder::FormBuilder do
  include_context 'setup builder'
  include_context 'setup examples'
  include_context 'setup radios'

  describe '#govuk_collection_radio_buttons' do
    let(:method) { :govuk_collection_radio_buttons }
    let(:colours) do
      [
        OpenStruct.new(id: 'red', name: 'Red', description: red_hint),
        OpenStruct.new(id: 'blue', name: 'Blue', description: blue_hint),
        OpenStruct.new(id: 'green', name: 'Green'),
        OpenStruct.new(id: 'yellow', name: 'Yellow')
      ]
    end

    let(:args) { [method, attribute, colours, :id, :name] }
    subject { builder.send(*args) }

    specify 'output should be a form group containing a form group and fieldset' do
      expect(subject).to have_tag('div', with: { class: 'govuk-form-group' }) do
        with_tag('fieldset', with: { class: 'govuk-fieldset' })
      end
    end

    include_examples 'HTML formatting checks'

    it_behaves_like 'a field that supports custom branding'

    it_behaves_like 'a field that supports custom classes' do
      let(:element) { 'div' }
      let(:default_classes) { %w(govuk-radios) }
    end

    it_behaves_like 'a field that supports a fieldset with legend' do
      let(:legend_text) { 'Pick your favourite colour' }
    end

    it_behaves_like 'a field that supports captions on the legend' do
      let(:legend_text) { 'Pick your favourite colour' }
    end

    it_behaves_like 'a field that supports hints' do
      let(:hint_text) { 'The colour of your favourite handkerchief' }
    end

    it_behaves_like 'a field that supports errors' do
      let(:error_message) { /Choose a favourite colour/ }
      let(:error_class) { nil }
      let(:error_identifier) { 'person-favourite-colour-error' }
    end

    it_behaves_like 'a field that accepts arbitrary blocks of HTML' do
      let(:described_element) { 'fieldset' }
    end

    it_behaves_like 'a field that supports setting the hint via localisation'
    it_behaves_like 'a field that supports setting the legend caption via localisation'

    it_behaves_like 'a field that supports localised collection labels' do
      let(:args) { [method, :department, departments, :code] }
    end

    it_behaves_like 'a field that supports localised collection hints' do
      let(:hint_class) { 'govuk-radios__hint' }
      let(:args) { [method, :department, departments, :code] }
    end

    it_behaves_like 'a field that supports setting the legend via localisation'
    it_behaves_like 'a field that contains a customisable form group'

    it_behaves_like 'a field that accepts a plain ruby object' do
      let(:described_element) { ['input', { with: { type: 'radio' }, count: colours.size }] }
    end

    describe 'radio buttons' do
      specify 'each radio button should have the correct classes' do
        expect(subject).to have_tag('input', with: { class: %w(govuk-radios__input) }, count: colours.size)
      end

      specify %(shouldn't have a hidden field by default) do
        expect(subject).to have_tag('fieldset') do
          with_tag('input', with: { type: 'hidden', name: "#{object_name}[#{attribute}]" })
        end
      end

      specify 'each label should have the correct classes' do
        expect(subject).to have_tag('label', count: colours.size, with: { class: %w(govuk-label govuk-radios__label) })
      end

      specify 'there should be the correct number' do
        expect(subject).to have_tag('input', count: colours.size, with: { type: 'radio' })
        expect(subject).to have_tag('label', count: colours.size)
      end

      specify 'radio buttons should be surrounded by a radios module' do
        expect(subject).to have_tag('div', with: { 'data-module' => 'govuk-radios' }) do |dm|
          expect(dm).to have_tag('input', count: colours.size, with: { type: 'radio' })
        end
      end

      specify 'each radio button should have the correct id' do
        colours.each do |colour|
          "person-favourite-colour-#{colour.id}-field".tap do |association|
            expect(subject).to have_tag('input', with: { id: association })
          end
        end
      end

      specify 'radio buttons should have the correct name' do
        names = parsed_subject.css('input').map { |e| e['name'] }

        expect(names).to all(eql('person[favourite_colour]'))
      end

      specify 'radio buttons should be associated with corresponding labels' do
        colours.each do |colour|
          "person-favourite-colour-#{colour.id}-field".tap do |association|
            expect(subject).to have_tag('input', with: { id: association })
            expect(subject).to have_tag('label', with: { for: association })
          end
        end
      end

      it_behaves_like 'a field that allows the label to be localised via a proc' do
        let(:i18n_proc) { ->(item) { I18n.t("colours.#{item.name.downcase}") } }
        let(:args) { [method, attribute, colours, :id, i18n_proc] }
      end

      it_behaves_like 'a collection field that supports setting the value via a proc'

      context 'radio button hints' do
        let(:colours_with_descriptions) { colours.select { |c| c.description.present? } }
        let(:colours_without_descriptions) { colours.reject { |c| c.description.present? } }

        subject { builder.send(*args.push(:description)) }

        specify 'the radio buttons with hints should contain hint text' do
          expect(subject).to have_tag('div', count: colours_with_descriptions.size, with: { class: 'govuk-hint govuk-radios__hint' })
        end

        specify 'the hint should be associated with the correct radio button' do
          colours_with_descriptions.each do |cwd|
            "person-favourite-colour-#{cwd.id}-hint".tap do |association|
              expect(subject).to have_tag('input', with: { "aria-describedby" => association })
              expect(subject).to have_tag('div', with: { class: 'govuk-hint', id: association })
            end
          end
        end

        specify 'radio buttons without hints should not have aria-describedby attributes' do
          colours_without_descriptions.each do |cwd|
            "person-favourite_colour-#{cwd.id}-hint".tap do |association|
              expect(subject).not_to have_tag('input', with: { "aria-describedby" => association })
            end
          end
        end

        specify 'all labels should be bold when hints are enabled' do
          expect(subject).to have_tag('label', with: { class: 'govuk-label--s' }, count: colours.size)
        end

        it_behaves_like 'a field that allows the hint to be localised via a proc' do
          let(:i18n_proc) { ->(item) { I18n.t("colours.#{item.name.downcase}") } }
          let(:args) { [method, attribute, colours, :id, :name, i18n_proc] }
        end
      end

      context 'layout direction' do
        context 'when inline is specified in the options' do
          subject do
            builder.send(*args.push(:description), inline: true)
          end

          specify "should have the additional class 'govuk-radios--inline'" do
            expect(subject).to have_tag('div', with: { class: %w(govuk-radios govuk-radios--inline) })
          end
        end

        context 'when inline is not specified in the options' do
          subject do
            builder.send(*args.push(:description), inline: false)
          end

          specify "should not have the additional class 'govuk-radios--inline'" do
            expect(parsed_subject.at_css('.govuk-radios')['class']).to eql('govuk-radios')
          end
        end
      end

      context 'radio button size' do
        context 'when small is specified in the options' do
          subject do
            builder.send(*args.push(:description), small: true)
          end

          specify "should have the additional class 'govuk-radios--small'" do
            expect(subject).to have_tag('div', with: { class: %w(govuk-radios govuk-radios--small) })
          end
        end

        context 'when small is not specified in the options' do
          subject do
            builder.send(*args.push(:description), small: false)
          end

          specify "should not have the additional class 'govuk-radios--small'" do
            expect(parsed_subject.at_css('.govuk-radios')['class']).to eql('govuk-radios')
          end
        end
      end

      context 'bold labels' do
        let(:bold_label_class) { 'govuk-label--s' }
        let(:bold_labels) { nil }
        let(:hint_method) { nil }

        subject do
          builder.send(*args.push(hint_method), bold_labels:)
        end

        context 'when bold_labels: nil' do
          context 'when a hint method is present' do
            let(:hint_method) { :description }

            specify 'labels should be bold to help them stand out from hints' do
              expect(subject).to have_tag('label', with: { class: bold_label_class }, count: colours.size)
            end
          end

          context 'when no hint method is present' do
            specify 'no labels should be set to bold' do
              expect(subject).not_to have_tag('label', with: { class: bold_label_class })
            end
          end
        end

        context 'when bold_labels: true' do
          let(:bold_labels) { true }

          specify 'all labels should be bold when hints are enabled' do
            expect(subject).to have_tag('label', with: { class: bold_label_class }, count: colours.size)
          end
        end

        context 'when bold_labels: false' do
          let(:bold_labels) { false }

          specify 'no labels should be set to bold' do
            expect(subject).not_to have_tag('label', with: { class: bold_label_class })
          end
        end
      end

      context 'generating a hidden field' do
        subject { builder.send(*args, include_hidden: false) }

        specify "the hidden field should be present within the fieldset" do
          expect(subject).not_to have_tag('input', with: { type: 'hidden' })
        end
      end
    end
  end
end