summaryrefslogtreecommitdiff
path: root/spec/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 11:10:13 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-20 11:10:13 +0000
commit0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch)
tree7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /spec/components
parent72123183a20411a36d607d70b12d57c484394c8e (diff)
downloadgitlab-ce-0ea3fcec397b69815975647f5e2aa5fe944a8486.tar.gz
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'spec/components')
-rw-r--r--spec/components/pajamas/alert_component_spec.rb18
-rw-r--r--spec/components/pajamas/banner_component_spec.rb169
-rw-r--r--spec/components/pajamas/button_component_spec.rb273
-rw-r--r--spec/components/pajamas/card_component_spec.rb80
-rw-r--r--spec/components/pajamas/checkbox_component_spec.rb130
-rw-r--r--spec/components/pajamas/component_spec.rb17
-rw-r--r--spec/components/pajamas/concerns/checkbox_radio_label_with_help_text_spec.rb110
-rw-r--r--spec/components/pajamas/concerns/checkbox_radio_options_spec.rb32
-rw-r--r--spec/components/pajamas/radio_component_spec.rb126
9 files changed, 948 insertions, 7 deletions
diff --git a/spec/components/pajamas/alert_component_spec.rb b/spec/components/pajamas/alert_component_spec.rb
index e596f07a15a..1e2845c44a8 100644
--- a/spec/components/pajamas/alert_component_spec.rb
+++ b/spec/components/pajamas/alert_component_spec.rb
@@ -50,10 +50,12 @@ RSpec.describe Pajamas::AlertComponent, :aggregate_failures, type: :component do
before do
render_inline described_class.new(
title: '_title_',
- alert_class: '_alert_class_',
- alert_data: {
- feature_id: '_feature_id_',
- dismiss_endpoint: '_dismiss_endpoint_'
+ alert_options: {
+ class: '_alert_class_',
+ data: {
+ feature_id: '_feature_id_',
+ dismiss_endpoint: '_dismiss_endpoint_'
+ }
}
)
end
@@ -106,9 +108,11 @@ RSpec.describe Pajamas::AlertComponent, :aggregate_failures, type: :component do
context 'with dismissible content' do
before do
render_inline described_class.new(
- close_button_class: '_close_button_class_',
- close_button_data: {
- testid: '_close_button_testid_'
+ close_button_options: {
+ class: '_close_button_class_',
+ data: {
+ testid: '_close_button_testid_'
+ }
}
)
end
diff --git a/spec/components/pajamas/banner_component_spec.rb b/spec/components/pajamas/banner_component_spec.rb
new file mode 100644
index 00000000000..5969f06dbad
--- /dev/null
+++ b/spec/components/pajamas/banner_component_spec.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::BannerComponent, type: :component do
+ subject do
+ described_class.new(**options)
+ end
+
+ let(:title) { "Banner title" }
+ let(:content) { "Banner content"}
+ let(:options) { {} }
+
+ describe 'basic usage' do
+ before do
+ render_inline(subject) do |c|
+ c.title { title }
+ content
+ end
+ end
+
+ it 'renders its content' do
+ expect(rendered_component).to have_text content
+ end
+
+ it 'renders its title' do
+ expect(rendered_component).to have_css "h1[class='gl-banner-title']", text: title
+ end
+
+ it 'renders a close button' do
+ expect(rendered_component).to have_css "button.gl-banner-close"
+ end
+
+ describe 'button_text and button_link' do
+ let(:options) { { button_text: 'Learn more', button_link: '/learn-more' } }
+
+ it 'define the primary action' do
+ expect(rendered_component).to have_css "a.btn-confirm.gl-button[href='/learn-more']", text: 'Learn more'
+ end
+ end
+
+ describe 'banner_options' do
+ let(:options) { { banner_options: { class: "baz", data: { foo: "bar" } } } }
+
+ it 'are on the banner' do
+ expect(rendered_component).to have_css ".gl-banner.baz[data-foo='bar']"
+ end
+
+ context 'with custom classes' do
+ let(:options) { { variant: :introduction, banner_options: { class: 'extra special' } } }
+
+ it 'don\'t conflict with internal banner_classes' do
+ expect(rendered_component).to have_css '.extra.special.gl-banner-introduction.gl-banner'
+ end
+ end
+ end
+
+ describe 'close_options' do
+ let(:options) { { close_options: { class: "js-foo", data: { uid: "123" } } } }
+
+ it 'are on the close button' do
+ expect(rendered_component).to have_css "button.gl-banner-close.js-foo[data-uid='123']"
+ end
+ end
+
+ describe 'embedded' do
+ context 'by default (false)' do
+ it 'keeps the banner\'s borders' do
+ expect(rendered_component).not_to have_css ".gl-banner.gl-border-none"
+ end
+ end
+
+ context 'when set to true' do
+ let(:options) { { embedded: true } }
+
+ it 'removes the banner\'s borders' do
+ expect(rendered_component).to have_css ".gl-banner.gl-border-none"
+ end
+ end
+ end
+
+ describe 'variant' do
+ context 'by default (promotion)' do
+ it 'applies no variant class' do
+ expect(rendered_component).to have_css "[class='gl-banner']"
+ end
+ end
+
+ context 'when set to introduction' do
+ let(:options) { { variant: :introduction } }
+
+ it "applies the introduction class to the banner" do
+ expect(rendered_component).to have_css ".gl-banner.gl-banner-introduction"
+ end
+
+ it "applies the confirm class to the close button" do
+ expect(rendered_component).to have_css ".gl-banner-close.btn-confirm.btn-confirm-tertiary"
+ end
+ end
+
+ context 'when set to unknown variant' do
+ let(:options) { { variant: :foobar } }
+
+ it 'ignores the unknown variant' do
+ expect(rendered_component).to have_css "[class='gl-banner']"
+ end
+ end
+ end
+
+ describe 'illustration' do
+ it 'has none by default' do
+ expect(rendered_component).not_to have_css ".gl-banner-illustration"
+ end
+
+ context 'with svg_path' do
+ let(:options) { { svg_path: 'logo.svg' } }
+
+ it 'renders an image as illustration' do
+ expect(rendered_component).to have_css ".gl-banner-illustration img"
+ end
+ end
+ end
+ end
+
+ context 'with illustration slot' do
+ before do
+ render_inline(subject) do |c|
+ c.title { title }
+ c.illustration { "<svg></svg>".html_safe }
+ content
+ end
+ end
+
+ it 'renders the slot content as illustration' do
+ expect(rendered_component).to have_css ".gl-banner-illustration svg"
+ end
+
+ context 'and conflicting svg_path' do
+ let(:options) { { svg_path: 'logo.svg' } }
+
+ it 'uses the slot content' do
+ expect(rendered_component).to have_css ".gl-banner-illustration svg"
+ expect(rendered_component).not_to have_css ".gl-banner-illustration img"
+ end
+ end
+ end
+
+ context 'with primary_action slot' do
+ before do
+ render_inline(subject) do |c|
+ c.title { title }
+ c.primary_action { "<a class='special' href='#'>Special</a>".html_safe }
+ content
+ end
+ end
+
+ it 'renders the slot content as the primary action' do
+ expect(rendered_component).to have_css "a.special", text: 'Special'
+ end
+
+ context 'and conflicting button_text and button_link' do
+ let(:options) { { button_text: 'Not special', button_link: '/' } }
+
+ it 'uses the slot content' do
+ expect(rendered_component).to have_css "a.special[href='#']", text: 'Special'
+ expect(rendered_component).not_to have_css "a.btn[href='/']"
+ end
+ end
+ end
+end
diff --git a/spec/components/pajamas/button_component_spec.rb b/spec/components/pajamas/button_component_spec.rb
new file mode 100644
index 00000000000..60c2a2e5a06
--- /dev/null
+++ b/spec/components/pajamas/button_component_spec.rb
@@ -0,0 +1,273 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::ButtonComponent, type: :component do
+ subject do
+ described_class.new(**options)
+ end
+
+ let(:content) { "Button content" }
+ let(:options) { {} }
+
+ describe 'basic usage' do
+ before do
+ render_inline(subject) do |c|
+ content
+ end
+ end
+
+ it 'renders its content' do
+ expect(rendered_component).to have_text content
+ end
+
+ it 'adds default styling' do
+ expect(rendered_component).to have_css ".btn.btn-default.btn-md.gl-button"
+ end
+
+ describe 'button_options' do
+ let(:options) { { button_options: { id: 'baz', data: { foo: 'bar' } } } }
+
+ it 'are added to the button' do
+ expect(rendered_component).to have_css ".gl-button#baz[data-foo='bar']"
+ end
+
+ context 'with custom classes' do
+ let(:options) { { variant: :danger, category: :tertiary, button_options: { class: 'custom-class' } } }
+
+ it 'don\'t conflict with internal button_classes' do
+ expect(rendered_component).to have_css '.gl-button.btn-danger.btn-danger-tertiary.custom-class'
+ end
+ end
+
+ context 'overriding base attributes' do
+ let(:options) { { button_options: { type: 'submit' } } }
+
+ it 'overrides type' do
+ expect(rendered_component).to have_css '[type="submit"]'
+ end
+ end
+ end
+
+ describe 'button_text_classes' do
+ let(:options) { { button_text_classes: 'custom-text-class' } }
+
+ it 'is added to the button text' do
+ expect(rendered_component).to have_css ".gl-button-text.custom-text-class"
+ end
+ end
+
+ describe 'disabled' do
+ context 'by default (false)' do
+ it 'does not have disabled styling and behavior' do
+ expect(rendered_component).not_to have_css ".disabled[disabled='disabled'][aria-disabled='true']"
+ end
+ end
+
+ context 'when set to true' do
+ let(:options) { { disabled: true } }
+
+ it 'has disabled styling and behavior' do
+ expect(rendered_component).to have_css ".disabled[disabled='disabled'][aria-disabled='true']"
+ end
+ end
+ end
+
+ describe 'loading' do
+ context 'by default (false)' do
+ it 'is not disabled' do
+ expect(rendered_component).not_to have_css ".disabled[disabled='disabled']"
+ end
+
+ it 'does not render a spinner' do
+ expect(rendered_component).not_to have_css ".gl-spinner[aria-label='Loading']"
+ end
+ end
+
+ context 'when set to true' do
+ let(:options) { { loading: true } }
+
+ it 'is disabled' do
+ expect(rendered_component).to have_css ".disabled[disabled='disabled']"
+ end
+
+ it 'renders a spinner' do
+ expect(rendered_component).to have_css ".gl-spinner[aria-label='Loading']"
+ end
+ end
+ end
+
+ describe 'block' do
+ context 'by default (false)' do
+ it 'is inline' do
+ expect(rendered_component).not_to have_css ".btn-block"
+ end
+ end
+
+ context 'when set to true' do
+ let(:options) { { block: true } }
+
+ it 'is block element' do
+ expect(rendered_component).to have_css ".btn-block"
+ end
+ end
+ end
+
+ describe 'selected' do
+ context 'by default (false)' do
+ it 'does not have selected styling and behavior' do
+ expect(rendered_component).not_to have_css ".selected"
+ end
+ end
+
+ context 'when set to true' do
+ let(:options) { { selected: true } }
+
+ it 'has selected styling and behavior' do
+ expect(rendered_component).to have_css ".selected"
+ end
+ end
+ end
+
+ describe 'category & variant' do
+ context 'with category variants' do
+ where(:variant) { [:default, :confirm, :danger] }
+
+ let(:options) { { variant: variant, category: :tertiary } }
+
+ with_them do
+ it 'renders the button in correct variant && category' do
+ expect(rendered_component).to have_css(".#{described_class::VARIANT_CLASSES[variant]}")
+ expect(rendered_component).to have_css(".#{described_class::VARIANT_CLASSES[variant]}-tertiary")
+ end
+ end
+ end
+
+ context 'with non-category variants' do
+ where(:variant) { [:dashed, :link, :reset] }
+
+ let(:options) { { variant: variant, category: :tertiary } }
+
+ with_them do
+ it 'renders the button in correct variant && category' do
+ expect(rendered_component).to have_css(".#{described_class::VARIANT_CLASSES[variant]}")
+ expect(rendered_component).not_to have_css(".#{described_class::VARIANT_CLASSES[variant]}-tertiary")
+ end
+ end
+ end
+
+ context 'with primary category' do
+ where(:variant) { [:default, :confirm, :danger] }
+
+ let(:options) { { variant: variant, category: :primary } }
+
+ with_them do
+ it 'renders the button in correct variant && category' do
+ expect(rendered_component).to have_css(".#{described_class::VARIANT_CLASSES[variant]}")
+ expect(rendered_component).not_to have_css(".#{described_class::VARIANT_CLASSES[variant]}-primary")
+ end
+ end
+ end
+ end
+
+ describe 'size' do
+ context 'by default (medium)' do
+ it 'applies medium class' do
+ expect(rendered_component).to have_css ".btn-md"
+ end
+ end
+
+ context 'when set to small' do
+ let(:options) { { size: :small } }
+
+ it "applies the small class to the button" do
+ expect(rendered_component).to have_css ".btn-sm"
+ end
+ end
+ end
+
+ describe 'icon' do
+ it 'has none by default' do
+ expect(rendered_component).not_to have_css ".gl-icon"
+ end
+
+ context 'with icon' do
+ let(:options) { { icon: 'star-o', icon_classes: 'custom-icon' } }
+
+ it 'renders an icon with custom CSS class' do
+ expect(rendered_component).to have_css "svg.gl-icon.gl-button-icon.custom-icon[data-testid='star-o-icon']"
+ expect(rendered_component).not_to have_css ".btn-icon"
+ end
+ end
+
+ context 'with icon only and no content' do
+ let(:content) { nil }
+ let(:options) { { icon: 'star-o' } }
+
+ it 'adds a "btn-icon" CSS class' do
+ expect(rendered_component).to have_css ".btn.btn-icon"
+ end
+ end
+
+ context 'with icon only and when loading' do
+ let(:content) { nil }
+ let(:options) { { icon: 'star-o', loading: true } }
+
+ it 'renders only a loading icon' do
+ expect(rendered_component).not_to have_css "svg.gl-icon.gl-button-icon.custom-icon[data-testid='star-o-icon']"
+ expect(rendered_component).to have_css ".gl-spinner[aria-label='Loading']"
+ end
+ end
+ end
+
+ describe 'type' do
+ context 'by default (without href)' do
+ it 'has type "button"' do
+ expect(rendered_component).to have_css "button[type='button']"
+ end
+ end
+
+ context 'when set to known type' do
+ where(:type) { [:button, :reset, :submit] }
+
+ let(:options) { { type: type } }
+
+ with_them do
+ it 'has the correct type' do
+ expect(rendered_component).to have_css "button[type='#{type}']"
+ end
+ end
+ end
+
+ context 'when set to unkown type' do
+ let(:options) { { type: :madeup } }
+
+ it 'has type "button"' do
+ expect(rendered_component).to have_css "button[type='button']"
+ end
+ end
+
+ context 'for links (with href)' do
+ let(:options) { { href: 'https://example.com', type: :reset } }
+
+ it 'ignores type' do
+ expect(rendered_component).not_to have_css "[type]"
+ end
+ end
+ end
+
+ describe 'link button' do
+ it 'renders a button tag with type="button" when "href" is not set' do
+ expect(rendered_component).to have_css "button[type='button']"
+ end
+
+ context 'when "href" is provided' do
+ let(:options) { { href: 'https://gitlab.com', target: '_blank' } }
+
+ it "renders a link instead of the button" do
+ expect(rendered_component).not_to have_css "button[type='button']"
+ expect(rendered_component).to have_css "a[href='https://gitlab.com'][target='_blank']"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/components/pajamas/card_component_spec.rb b/spec/components/pajamas/card_component_spec.rb
new file mode 100644
index 00000000000..65522a9023f
--- /dev/null
+++ b/spec/components/pajamas/card_component_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::CardComponent, :aggregate_failures, type: :component do
+ let(:header) { 'Card header' }
+ let(:body) { 'Card body' }
+ let(:footer) { 'Card footer' }
+
+ context 'slots' do
+ before do
+ render_inline described_class.new do |c|
+ c.header { header }
+ c.body { body }
+ c.footer { footer }
+ end
+ end
+
+ it 'renders card header' do
+ expect(rendered_component).to have_content(header)
+ end
+
+ it 'renders card body' do
+ expect(rendered_component).to have_content(body)
+ end
+
+ it 'renders footer' do
+ expect(rendered_component).to have_content(footer)
+ end
+ end
+
+ context 'with defaults' do
+ before do
+ render_inline described_class.new
+ end
+
+ it 'does not have a header or footer' do
+ expect(rendered_component).not_to have_selector('.gl-card-header')
+ expect(rendered_component).not_to have_selector('.gl-card-footer')
+ end
+
+ it 'renders the card and body' do
+ expect(rendered_component).to have_selector('.gl-card')
+ expect(rendered_component).to have_selector('.gl-card-body')
+ end
+ end
+
+ context 'with custom options' do
+ before do
+ render_inline described_class.new(
+ card_options: { class: '_card_class_', data: { testid: '_card_testid_' } },
+ header_options: { class: '_header_class_', data: { testid: '_header_testid_' } },
+ body_options: { class: '_body_class_', data: { testid: '_body_testid_' } },
+ footer_options: { class: '_footer_class_', data: { testid: '_footer_testid_' } }) do |c|
+ c.header { header }
+ c.body { body }
+ c.footer { footer }
+ end
+ end
+
+ it 'renders card options' do
+ expect(rendered_component).to have_selector('._card_class_')
+ expect(rendered_component).to have_selector('[data-testid="_card_testid_"]')
+ end
+
+ it 'renders header options' do
+ expect(rendered_component).to have_selector('._header_class_')
+ expect(rendered_component).to have_selector('[data-testid="_header_testid_"]')
+ end
+
+ it 'renders body options' do
+ expect(rendered_component).to have_selector('._body_class_')
+ expect(rendered_component).to have_selector('[data-testid="_body_testid_"]')
+ end
+
+ it 'renders footer options' do
+ expect(rendered_component).to have_selector('._footer_class_')
+ expect(rendered_component).to have_selector('[data-testid="_footer_testid_"]')
+ end
+ end
+end
diff --git a/spec/components/pajamas/checkbox_component_spec.rb b/spec/components/pajamas/checkbox_component_spec.rb
new file mode 100644
index 00000000000..b2f3a84fbfe
--- /dev/null
+++ b/spec/components/pajamas/checkbox_component_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::CheckboxComponent, :aggregate_failures, type: :component do
+ include FormBuilderHelpers
+
+ let_it_be(:method) { :view_diffs_file_by_file }
+ let_it_be(:label) { "Show one file at a time on merge request's Changes tab" }
+ let_it_be(:help_text) { 'Instead of all the files changed, show only one file at a time.' }
+
+ RSpec.shared_examples 'it renders unchecked checkbox with value of `1`' do
+ it 'renders unchecked checkbox with value of `1`' do
+ expect(rendered_component).to have_unchecked_field(label, with: '1')
+ end
+ end
+
+ context 'with default options' do
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method,
+ label: label
+ )
+ )
+ end
+ end
+
+ include_examples 'it renders unchecked checkbox with value of `1`'
+ include_examples 'it does not render help text'
+
+ it 'renders hidden input with value of `0`' do
+ expect(rendered_component).to have_field('user[view_diffs_file_by_file]', type: 'hidden', with: '0')
+ end
+ end
+
+ context 'with custom options' do
+ let_it_be(:checked_value) { 'yes' }
+ let_it_be(:unchecked_value) { 'no' }
+ let_it_be(:checkbox_options) { { class: 'checkbox-foo-bar', checked: true } }
+ let_it_be(:label_options) { { class: 'label-foo-bar' } }
+
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method,
+ label: label,
+ help_text: help_text,
+ checked_value: checked_value,
+ unchecked_value: unchecked_value,
+ checkbox_options: checkbox_options,
+ label_options: label_options
+ )
+ )
+ end
+ end
+
+ include_examples 'it renders help text'
+
+ it 'renders checked checkbox with value of `yes`' do
+ expect(rendered_component).to have_checked_field(label, with: checked_value, class: checkbox_options[:class])
+ end
+
+ it 'adds CSS class to label' do
+ expect(rendered_component).to have_selector('label.label-foo-bar')
+ end
+
+ it 'renders hidden input with value of `no`' do
+ expect(rendered_component).to have_field('user[view_diffs_file_by_file]', type: 'hidden', with: unchecked_value)
+ end
+ end
+
+ context 'with `label` slot' do
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method
+ )
+ ) do |c|
+ c.label { label }
+ end
+ end
+ end
+
+ include_examples 'it renders unchecked checkbox with value of `1`'
+ end
+
+ context 'with `help_text` slot' do
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method,
+ label: label
+ )
+ ) do |c|
+ c.help_text { help_text }
+ end
+ end
+ end
+
+ include_examples 'it renders unchecked checkbox with value of `1`'
+ include_examples 'it renders help text'
+ end
+
+ context 'with `label` and `help_text` slots' do
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method
+ )
+ ) do |c|
+ c.label { label }
+ c.help_text { help_text }
+ end
+ end
+ end
+
+ include_examples 'it renders unchecked checkbox with value of `1`'
+ include_examples 'it renders help text'
+ end
+end
diff --git a/spec/components/pajamas/component_spec.rb b/spec/components/pajamas/component_spec.rb
index 96f6b43bac1..7385519b468 100644
--- a/spec/components/pajamas/component_spec.rb
+++ b/spec/components/pajamas/component_spec.rb
@@ -23,4 +23,21 @@ RSpec.describe Pajamas::Component do
expect(value).to eq('something')
end
end
+
+ describe '#format_options' do
+ it 'merges CSS classes and additional options' do
+ expect(
+ subject.send(
+ :format_options,
+ options: { foo: 'bar', class: 'gl-display-flex gl-py-5' },
+ css_classes: %w(gl-px-5 gl-mt-5),
+ additional_options: { baz: 'bax' }
+ )
+ ).to match({
+ foo: 'bar',
+ baz: 'bax',
+ class: ['gl-px-5', 'gl-mt-5', 'gl-display-flex gl-py-5']
+ })
+ end
+ end
end
diff --git a/spec/components/pajamas/concerns/checkbox_radio_label_with_help_text_spec.rb b/spec/components/pajamas/concerns/checkbox_radio_label_with_help_text_spec.rb
new file mode 100644
index 00000000000..7a792592b3c
--- /dev/null
+++ b/spec/components/pajamas/concerns/checkbox_radio_label_with_help_text_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::Concerns::CheckboxRadioLabelWithHelpText do
+ let(:form) { instance_double('ActionView::Helpers::FormBuilder') }
+ let(:component_class) do
+ Class.new do
+ attr_reader(
+ :form,
+ :method,
+ :label_argument,
+ :help_text_argument,
+ :label_options,
+ :input_options,
+ :value
+ )
+
+ def initialize(
+ form:,
+ method:,
+ label: nil,
+ help_text: nil,
+ label_options: {},
+ radio_options: {},
+ value: nil
+ )
+ @form = form
+ @method = method
+ @label_argument = label
+ @help_text_argument = help_text
+ @label_options = label_options
+ @input_options = radio_options
+ @value = value
+ end
+
+ def label_content
+ @label_argument
+ end
+
+ def help_text_content
+ @help_text_argument
+ end
+
+ def format_options(options:, css_classes: [], additional_options: {})
+ {}
+ end
+
+ include Pajamas::Concerns::CheckboxRadioLabelWithHelpText
+ include ActionView::Helpers::TagHelper
+ end
+ end
+
+ let_it_be(:method) { 'username' }
+ let_it_be(:label_options) { { class: 'foo-bar' } }
+ let_it_be(:value) { 'Foo bar' }
+
+ describe '#render_label_with_help_text' do
+ it 'calls `#format_options` with correct arguments' do
+ allow(form).to receive(:label)
+
+ component = component_class.new(form: form, method: method, label_options: label_options, value: value)
+
+ expect(component).to receive(:format_options).with(
+ options: label_options,
+ css_classes: ['custom-control-label'],
+ additional_options: { value: value }
+ )
+
+ component.render_label_with_help_text
+ end
+
+ context 'when `help_text` argument is passed' do
+ it 'calls `form.label` with `label` and `help_text` arguments used in the block' do
+ component = component_class.new(
+ form: form,
+ method: method,
+ label: 'Label argument',
+ help_text: 'Help text argument'
+ )
+
+ expected_label_entry = '<span>Label argument</span><p class="help-text"' \
+ ' data-testid="pajamas-component-help-text">Help text argument</p>'
+
+ expect(form).to receive(:label).with(method, {}) do |&block|
+ expect(block.call).to eq(expected_label_entry)
+ end
+
+ component.render_label_with_help_text
+ end
+ end
+
+ context 'when `help_text` argument is not passed' do
+ it 'calls `form.label` with `label` argument used in the block' do
+ component = component_class.new(
+ form: form,
+ method: method,
+ label: 'Label argument'
+ )
+
+ expected_label_entry = '<span>Label argument</span>'
+
+ expect(form).to receive(:label).with(method, {}) do |&block|
+ expect(block.call).to eq(expected_label_entry)
+ end
+
+ component.render_label_with_help_text
+ end
+ end
+ end
+end
diff --git a/spec/components/pajamas/concerns/checkbox_radio_options_spec.rb b/spec/components/pajamas/concerns/checkbox_radio_options_spec.rb
new file mode 100644
index 00000000000..3eb888e5f3b
--- /dev/null
+++ b/spec/components/pajamas/concerns/checkbox_radio_options_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::Concerns::CheckboxRadioOptions do
+ let(:component_class) do
+ Class.new do
+ include Pajamas::Concerns::CheckboxRadioOptions
+
+ attr_reader(:input_options)
+
+ def initialize(input_options: {})
+ @input_options = input_options
+ end
+
+ def format_options(options:, css_classes: [], additional_options: {})
+ {}
+ end
+ end
+ end
+
+ describe '#formatted_input_options' do
+ let_it_be(:input_options) { { class: 'foo-bar' } }
+
+ it 'calls `#format_options` with correct arguments' do
+ component = component_class.new(input_options: input_options)
+
+ expect(component).to receive(:format_options).with(options: input_options, css_classes: ['custom-control-input'])
+
+ component.formatted_input_options
+ end
+ end
+end
diff --git a/spec/components/pajamas/radio_component_spec.rb b/spec/components/pajamas/radio_component_spec.rb
new file mode 100644
index 00000000000..3885d101c7a
--- /dev/null
+++ b/spec/components/pajamas/radio_component_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::RadioComponent, :aggregate_failures, type: :component do
+ include FormBuilderHelpers
+
+ let_it_be(:method) { :access_level }
+ let_it_be(:label) { "Access Level" }
+ let_it_be(:value) { :regular }
+ let_it_be(:help_text) do
+ 'Administrators have access to all groups, projects, and users and can manage all features in this installation'
+ end
+
+ RSpec.shared_examples 'it renders unchecked radio' do
+ it 'renders unchecked radio' do
+ expect(rendered_component).to have_unchecked_field(label)
+ end
+ end
+
+ context 'with default options' do
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method,
+ value: value,
+ label: label
+ )
+ )
+ end
+ end
+
+ include_examples 'it renders unchecked radio'
+ include_examples 'it does not render help text'
+ end
+
+ context 'with custom options' do
+ let_it_be(:radio_options) { { class: 'radio-foo-bar', checked: true } }
+ let_it_be(:label_options) { { class: 'label-foo-bar' } }
+
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method,
+ value: method,
+ label: label,
+ help_text: help_text,
+ radio_options: radio_options,
+ label_options: label_options
+ )
+ )
+ end
+ end
+
+ include_examples 'it renders help text'
+
+ it 'renders checked radio' do
+ expect(rendered_component).to have_checked_field(label, class: radio_options[:class])
+ end
+
+ it 'adds CSS class to label' do
+ expect(rendered_component).to have_selector('label.label-foo-bar')
+ end
+ end
+
+ context 'with `label` slot' do
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method,
+ value: value
+ )
+ ) do |c|
+ c.label { label }
+ end
+ end
+ end
+
+ include_examples 'it renders unchecked radio'
+ end
+
+ context 'with `help_text` slot' do
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method,
+ value: value,
+ label: label
+ )
+ ) do |c|
+ c.help_text { help_text }
+ end
+ end
+ end
+
+ include_examples 'it renders unchecked radio'
+ include_examples 'it renders help text'
+ end
+
+ context 'with `label` and `help_text` slots' do
+ before do
+ fake_form_for do |form|
+ render_inline(
+ described_class.new(
+ form: form,
+ method: method,
+ value: value
+ )
+ ) do |c|
+ c.label { label }
+ c.help_text { help_text }
+ end
+ end
+ end
+
+ include_examples 'it renders unchecked radio'
+ include_examples 'it renders help text'
+ end
+end