blacklight 9.0.0.beta6 → 9.0.0.beta8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.docker/app/Dockerfile +2 -1
  3. data/.github/matrix.json +21 -3
  4. data/.solr_wrapper.yml +1 -1
  5. data/VERSION +1 -1
  6. data/app/assets/javascripts/blacklight/blacklight.esm.js +9 -5
  7. data/app/assets/javascripts/blacklight/blacklight.esm.js.map +1 -1
  8. data/app/assets/javascripts/blacklight/blacklight.js +9 -5
  9. data/app/assets/javascripts/blacklight/blacklight.js.map +1 -1
  10. data/app/components/blacklight/advanced_search_form_component.rb +2 -1
  11. data/app/components/blacklight/constraints_component.html.erb +3 -8
  12. data/app/components/blacklight/constraints_component.rb +57 -14
  13. data/app/components/blacklight/document/bookmark_component.rb +1 -1
  14. data/app/components/blacklight/document/page_header_component.rb +1 -1
  15. data/app/components/blacklight/document_component.rb +20 -24
  16. data/app/components/blacklight/facet_item_pivot_component.rb +4 -0
  17. data/app/components/blacklight/facets/filters_component.rb +1 -1
  18. data/app/components/blacklight/facets/item_component.rb +3 -0
  19. data/app/components/blacklight/facets/selected_value_component.rb +1 -1
  20. data/app/components/blacklight/facets/suggest_component.rb +2 -3
  21. data/app/components/blacklight/header_component.rb +2 -2
  22. data/app/components/blacklight/metadata_field_component.html.erb +2 -2
  23. data/app/components/blacklight/metadata_field_component.rb +2 -1
  24. data/app/components/blacklight/metadata_field_layout_component.rb +9 -4
  25. data/app/components/blacklight/search_bar_component.html.erb +1 -1
  26. data/app/controllers/concerns/blacklight/catalog.rb +8 -4
  27. data/app/javascript/blacklight-frontend/facet_suggest.js +2 -1
  28. data/app/javascript/blacklight-frontend/modal.js +7 -4
  29. data/app/presenters/blacklight/constraint_presenter.rb +22 -0
  30. data/app/presenters/blacklight/document_presenter.rb +6 -5
  31. data/app/presenters/blacklight/facet_field_presenter.rb +10 -3
  32. data/app/presenters/blacklight/facet_item_pivot_presenter.rb +1 -5
  33. data/app/presenters/blacklight/facet_item_presenter.rb +0 -18
  34. data/app/presenters/blacklight/field_presenter.rb +4 -2
  35. data/app/presenters/blacklight/rendering/abstract_step.rb +7 -1
  36. data/app/presenters/blacklight/rendering/join.rb +9 -5
  37. data/app/presenters/blacklight/rendering/terminator.rb +1 -1
  38. data/app/views/catalog/_document.atom.builder +1 -1
  39. data/app/views/catalog/_document.html.erb +1 -1
  40. data/app/views/catalog/_show_main_content.html.erb +9 -5
  41. data/app/views/catalog/index.html.erb +0 -1
  42. data/app/views/catalog/show.html.erb +2 -2
  43. data/blacklight.gemspec +1 -1
  44. data/config/locales/blacklight.ar.yml +1 -2
  45. data/config/locales/blacklight.ca.yml +1 -2
  46. data/config/locales/blacklight.de.yml +1 -2
  47. data/config/locales/blacklight.en.yml +2 -3
  48. data/config/locales/blacklight.es.yml +1 -2
  49. data/config/locales/blacklight.fr.yml +1 -2
  50. data/config/locales/blacklight.hu.yml +1 -2
  51. data/config/locales/blacklight.it.yml +1 -2
  52. data/config/locales/blacklight.nl.yml +1 -2
  53. data/config/locales/blacklight.pt-BR.yml +1 -2
  54. data/config/locales/blacklight.sq.yml +1 -2
  55. data/config/locales/blacklight.zh.yml +1 -2
  56. data/lib/blacklight/component.rb +7 -1
  57. data/lib/blacklight/configuration/facet_field.rb +4 -0
  58. data/lib/blacklight/configuration/view_config.rb +30 -16
  59. data/lib/blacklight/configuration.rb +58 -5
  60. data/lib/blacklight/search_state/pivot_filter_field.rb +1 -1
  61. data/lib/blacklight/solr/field_reflection_search_builder.rb +11 -0
  62. data/lib/blacklight/solr/repository.rb +5 -5
  63. data/lib/blacklight/solr/search_builder_behavior.rb +19 -2
  64. data/lib/blacklight/solr/single_doc_search_builder.rb +25 -0
  65. data/lib/generators/blacklight/templates/.solr_wrapper.yml +1 -1
  66. data/lib/generators/blacklight/templates/catalog_controller.rb +26 -4
  67. data/lib/generators/blacklight/templates/solr/conf/solrconfig.xml +0 -67
  68. data/package.json +1 -1
  69. data/spec/components/blacklight/constraints_component_spec.rb +2 -2
  70. data/spec/components/blacklight/document_component_spec.rb +8 -15
  71. data/spec/components/blacklight/facets/filters_component_spec.rb +2 -2
  72. data/spec/components/blacklight/facets/index_navigation_component_spec.rb +2 -1
  73. data/spec/components/blacklight/facets/suggest_component_spec.rb +15 -1
  74. data/spec/components/blacklight/search_bar_component_spec.rb +24 -1
  75. data/spec/controllers/blacklight/catalog_spec.rb +1 -1
  76. data/spec/features/advanced_search_spec.rb +39 -20
  77. data/spec/features/search_filters_spec.rb +3 -3
  78. data/spec/features/search_spec.rb +3 -3
  79. data/spec/models/blacklight/configuration_spec.rb +126 -0
  80. data/spec/models/blacklight/solr/repository_spec.rb +6 -0
  81. data/spec/models/blacklight/solr/search_builder_behavior_spec.rb +52 -6
  82. data/spec/presenters/blacklight/constraint_presenter_spec.rb +32 -0
  83. data/spec/presenters/blacklight/document_presenter_spec.rb +3 -3
  84. data/spec/presenters/blacklight/facet_item_presenter_spec.rb +0 -7
  85. data/spec/presenters/blacklight/field_presenter_spec.rb +103 -22
  86. data/spec/presenters/blacklight/rendering/pipeline_spec.rb +130 -14
  87. data/spec/support/presenter_test_helpers.rb +1 -1
  88. data/spec/views/catalog/index.atom.builder_spec.rb +2 -0
  89. metadata +18 -10
  90. data/app/views/shared/_sitelinks_search_box.html.erb +0 -12
  91. data/spec/features/sitelinks_search_box_spec.rb +0 -13
@@ -735,4 +735,130 @@ RSpec.describe Blacklight::Configuration, :api do
735
735
  described_class.default_configuration.delete_at(2)
736
736
  end
737
737
  end
738
+
739
+ describe "#copy_search_field_config_to_advanced!" do
740
+ let(:config) { described_class.new }
741
+
742
+ before do
743
+ config.add_search_field('title',
744
+ solr_parameters: {
745
+ 'spellcheck.dictionary': 'title',
746
+ qf: '${title_qf}',
747
+ pf: '${title_pf}'
748
+ })
749
+ config.add_search_field('excluded_field',
750
+ solr_parameters: { qf: '${excluded_qf}' },
751
+ include_in_advanced_search: false)
752
+ config.add_search_field('already_configured',
753
+ solr_parameters: { qf: '${configured_qf}' },
754
+ clause_params: { edismax: { existing_custom: 'params' } })
755
+ end
756
+
757
+ it "copies solr_parameters to clause_params for eligible search fields" do
758
+ config.copy_search_field_config_to_advanced!
759
+
760
+ title_field = config.search_fields['title']
761
+ expect(title_field.clause_params).to be_present
762
+ expect(title_field.clause_params[:edismax]).to eq({ 'spellcheck.dictionary': 'title', qf: '${title_qf}', pf: '${title_pf}' })
763
+ end
764
+
765
+ it "skips fields with include_in_advanced_search set to false" do
766
+ config.copy_search_field_config_to_advanced!
767
+
768
+ excluded_field = config.search_fields['excluded_field']
769
+ expect(excluded_field.clause_params).to be_nil
770
+ end
771
+
772
+ it "skips fields that already have a clause_params config" do
773
+ config.copy_search_field_config_to_advanced!
774
+
775
+ already_configured_field = config.search_fields['already_configured']
776
+ expect(already_configured_field.clause_params).to eq({ edismax: { existing_custom: 'params' } })
777
+ end
778
+
779
+ it "handles fields with nil solr_parameters" do
780
+ config.add_search_field('no_solr_params')
781
+
782
+ expect { config.copy_search_field_config_to_advanced! }.not_to raise_error
783
+
784
+ field = config.search_fields['no_solr_params']
785
+ expect(field.clause_params).to be_present
786
+ expect(field.clause_params[:edismax]).to eq({})
787
+ end
788
+ end
789
+
790
+ describe "#copy_facet_field_config_to_advanced!" do
791
+ let(:config) { described_class.new }
792
+
793
+ before do
794
+ config.add_facet_field('format',
795
+ field: 'format')
796
+ config.add_facet_field('subject_ssim')
797
+ config.add_facet_field('excluded_facet',
798
+ field: 'excluded_field',
799
+ include_in_advanced_search: false)
800
+ config.add_facet_field('query_facet',
801
+ query: { 'recent' => { fq: 'pub_date_ssim:[2020 TO *]' } })
802
+ config.add_facet_field('pivot_facet',
803
+ pivot: %w[author_ssim subject_ssim])
804
+ config.add_facet_field('range_facet',
805
+ range: true,
806
+ field: 'pub_date_ssim')
807
+ end
808
+
809
+ it "sets default facet.sort to 'count'" do
810
+ config.copy_facet_field_config_to_advanced!
811
+
812
+ expect(config.advanced_search.form_solr_parameters['facet.sort']).to eq 'count'
813
+ end
814
+
815
+ it "adds eligible facet fields to facet.field array" do
816
+ config.copy_facet_field_config_to_advanced!
817
+
818
+ facet_fields = config.advanced_search.form_solr_parameters['facet.field']
819
+ expect(facet_fields).to eq(%w[format subject_ssim])
820
+ end
821
+
822
+ it "skips fields with include_in_advanced_search set to false" do
823
+ config.copy_facet_field_config_to_advanced!
824
+
825
+ facet_fields = config.advanced_search.form_solr_parameters['facet.field']
826
+ expect(facet_fields).not_to include('excluded_field')
827
+ end
828
+
829
+ it "skips fields that are query, pivot, or range facets" do
830
+ config.copy_facet_field_config_to_advanced!
831
+
832
+ facet_fields = config.advanced_search.form_solr_parameters['facet.field']
833
+ expect(facet_fields).not_to include('query_facet', 'pivot_facet', 'range_facet')
834
+ end
835
+
836
+ it "sets facet limit to -1 to show all values for eligible fields" do
837
+ config.copy_facet_field_config_to_advanced!
838
+
839
+ expect(config.advanced_search.form_solr_parameters['f.format.facet.limit']).to eq(-1)
840
+ expect(config.advanced_search.form_solr_parameters['f.subject_ssim.facet.limit']).to eq(-1)
841
+ end
842
+
843
+ context "preserving existing advanced search config if present" do
844
+ before do
845
+ config.advanced_search.form_solr_parameters = {
846
+ 'facet.sort' => 'index',
847
+ 'f.subject_ssim.facet.limit' => 50
848
+ }
849
+ end
850
+
851
+ it "preserves existing facet.limit configuration if set" do
852
+ config.copy_facet_field_config_to_advanced!
853
+
854
+ expect(config.advanced_search.form_solr_parameters['f.subject_ssim.facet.limit']).to eq(50)
855
+ end
856
+
857
+ it "preserves existing default facet.sort configuration if set" do
858
+ config.copy_facet_field_config_to_advanced!
859
+
860
+ expect(config.advanced_search.form_solr_parameters['facet.sort']).to eq('index')
861
+ end
862
+ end
863
+ end
738
864
  end
@@ -87,6 +87,12 @@ RSpec.describe Blacklight::Solr::Repository, :api do
87
87
  expect(subject.search(params: {})).to be_a Blacklight::Solr::Response
88
88
  end
89
89
 
90
+ it "can be called with no args" do
91
+ blacklight_config.solr_path = 'xyz'
92
+ allow(subject.connection).to receive(:send_and_receive).with('xyz', anything).and_return(mock_response)
93
+ expect(subject.search).to be_a Blacklight::Solr::Response
94
+ end
95
+
90
96
  it "uses the default solr path" do
91
97
  allow(subject.connection).to receive(:send_and_receive).with('select', anything).and_return(mock_response)
92
98
  expect(subject.search(params: {})).to be_a Blacklight::Solr::Response
@@ -523,6 +523,58 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, :api do
523
523
  end
524
524
  end
525
525
 
526
+ describe "#add_facets_for_advanced_search_form" do
527
+ let(:solr_parameters) { Blacklight::Solr::Request.new }
528
+ let(:mock_controller) { instance_double(CatalogController, action_name: 'advanced_search') }
529
+ let(:mock_search_state) { instance_double(Blacklight::SearchState, controller: mock_controller) }
530
+
531
+ before do
532
+ allow(search_builder).to receive(:search_state).and_return(mock_search_state)
533
+ end
534
+
535
+ context "when action is advanced_search and form_solr_parameters are configured" do
536
+ before do
537
+ blacklight_config.advanced_search.enabled = true
538
+ blacklight_config.advanced_search.form_solr_parameters = {
539
+ 'facet.sort' => 'count',
540
+ 'facet.field' => %w[format subject_ssim],
541
+ 'f.format.facet.limit' => -1,
542
+ 'f.subject_ssim.facet.limit' => -1
543
+ }
544
+ end
545
+
546
+ it "merges advanced search form parameters into solr parameters" do
547
+ solr_parameters['existing.param'] = 'keep_me'
548
+ subject.add_facets_for_advanced_search_form(solr_parameters)
549
+
550
+ expect(solr_parameters['facet.sort']).to eq 'count'
551
+ expect(solr_parameters['facet.field']).to eq %w[format subject_ssim]
552
+ expect(solr_parameters['f.format.facet.limit']).to eq(-1)
553
+ expect(solr_parameters['f.subject_ssim.facet.limit']).to eq(-1)
554
+ expect(solr_parameters['existing.param']).to eq 'keep_me'
555
+ end
556
+ end
557
+
558
+ context "when action is not advanced_search" do
559
+ before do
560
+ allow(mock_controller).to receive(:action_name).and_return('index')
561
+ blacklight_config.advanced_search.enabled = true
562
+ blacklight_config.advanced_search.form_solr_parameters = {
563
+ 'facet.sort' => 'count',
564
+ 'facet.field' => ['format']
565
+ }
566
+ end
567
+
568
+ it "does not merge advanced search form parameters" do
569
+ solr_parameters['f.format.facet.limit'] = 21
570
+
571
+ subject.add_facets_for_advanced_search_form(solr_parameters)
572
+
573
+ expect(solr_parameters['f.format.facet.limit']).to eq 21
574
+ end
575
+ end
576
+ end
577
+
526
578
  describe "#add_facet_fq_to_solr" do
527
579
  it "converts a String fq into an Array" do
528
580
  solr_parameters = { fq: 'a string' }
@@ -677,12 +729,6 @@ RSpec.describe Blacklight::Solr::SearchBuilderBehavior, :api do
677
729
  end
678
730
 
679
731
  context 'with advanced search clause parameters' do
680
- before do
681
- blacklight_config.search_fields.each_value do |v|
682
- v.clause_params = { edismax: v.solr_parameters.dup }
683
- end
684
- end
685
-
686
732
  let(:user_params) { { op: 'must', clause: { '0': { field: 'title', query: 'the book' }, '1': { field: 'author', query: 'the person' } } } }
687
733
 
688
734
  it "has proper solr parameters" do
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Blacklight::ConstraintPresenter, type: :presenter do
6
+ subject(:presenter) do
7
+ described_class.new(facet_item_presenter: facet_item_presenter, field_label: field_label)
8
+ end
9
+
10
+ let(:facet_item_presenter) do
11
+ instance_double(Blacklight::FacetItemPresenter, label: 'item 1', remove_href: '/catalog')
12
+ end
13
+ let(:field_label) { 'field 1' }
14
+
15
+ describe '#constraint_label' do
16
+ subject { presenter.constraint_label }
17
+
18
+ it { is_expected.to eq 'item 1' }
19
+ end
20
+
21
+ describe '#field_label' do
22
+ subject { presenter.field_label }
23
+
24
+ it { is_expected.to eq 'field 1' }
25
+ end
26
+
27
+ describe '#remove_href' do
28
+ subject { presenter.remove_href }
29
+
30
+ it { is_expected.to eq '/catalog' }
31
+ end
32
+ end
@@ -29,13 +29,13 @@ RSpec.describe Blacklight::DocumentPresenter do
29
29
  end
30
30
 
31
31
  describe '#field_value' do
32
- let(:field_presenter) { instance_double(Blacklight::FieldPresenter, render: 'xyz') }
32
+ let(:field_presenter) { instance_double(Blacklight::FieldPresenter, render: ['xyz']) }
33
33
  let(:field_config) { Blacklight::Configuration::DisplayField.new }
34
34
  let(:options) { { a: 1 } }
35
35
 
36
36
  it 'calls the field presenter' do
37
37
  allow(Blacklight::FieldPresenter).to receive(:new).with(request_context, doc, field_config, options).and_return(field_presenter)
38
- expect(presenter.field_value(field_config, options)).to eq 'xyz'
38
+ expect(presenter.field_value(field_config, options)).to eq ['xyz']
39
39
  end
40
40
 
41
41
  it 'can be configured to use an alternate presenter' do
@@ -44,7 +44,7 @@ RSpec.describe Blacklight::DocumentPresenter do
44
44
  field_config.presenter = SomePresenter
45
45
  allow(SomePresenter).to receive(:new).and_return(instance)
46
46
 
47
- expect(presenter.field_value(field_config, options)).to eq 'abc'
47
+ expect(presenter.field_value(field_config, options)).to eq ['abc']
48
48
  end
49
49
  end
50
50
 
@@ -56,13 +56,6 @@ RSpec.describe Blacklight::FacetItemPresenter, type: :presenter do
56
56
  end
57
57
  end
58
58
 
59
- describe '#constraint_label' do
60
- it 'provides the label for the constraint' do
61
- allow(facet_config).to receive_messages(query: nil, date: nil, helper_method: nil, url_method: nil)
62
- expect(presenter.constraint_label).to eq presenter.label
63
- end
64
- end
65
-
66
59
  describe '#href' do
67
60
  let(:filter_field) { instance_double(Blacklight::SearchState::FilterField, include?: false) }
68
61
 
@@ -6,6 +6,8 @@ RSpec.describe Blacklight::FieldPresenter, :api do
6
6
  let(:request_context) { double('View context', params: { x: '1' }, search_state: search_state, should_render_field?: true, blacklight_config: config) }
7
7
  let(:document) do
8
8
  SolrDocument.new(id: 1,
9
+ 'join_true' => %w[a b c],
10
+ 'separator_options' => %w[d e f],
9
11
  'link_to_facet_true' => 'x',
10
12
  'link_to_facet_named' => 'x',
11
13
  'qwer' => 'document qwer value',
@@ -30,6 +32,8 @@ RSpec.describe Blacklight::FieldPresenter, :api do
30
32
  Blacklight::Configuration.new.configure do |config|
31
33
  config.add_index_field 'qwer'
32
34
  config.add_index_field 'asdf', helper_method: :render_asdf_index_field
35
+ config.add_index_field 'join_true', join: true
36
+ config.add_index_field 'separator_options', separator_options: { words_connector: '<br/>', last_word_connector: '<br/>' }
33
37
  config.add_index_field 'link_to_facet_true', link_to_facet: true
34
38
  config.add_index_field 'link_to_facet_named', link_to_facet: :some_field
35
39
  config.add_index_field 'highlight', highlight: true
@@ -40,6 +44,7 @@ RSpec.describe Blacklight::FieldPresenter, :api do
40
44
  config.add_index_field 'alias', field: 'qwer'
41
45
  config.add_index_field 'with_default', default: 'value'
42
46
  config.add_index_field 'with_steps', steps: [custom_step]
47
+ config.add_index_field :some_field
43
48
  config.add_facet_field :link_to_facet_true
44
49
  config.add_facet_field :some_field
45
50
  end
@@ -51,25 +56,33 @@ RSpec.describe Blacklight::FieldPresenter, :api do
51
56
  context 'when an explicit html value is provided' do
52
57
  let(:options) { { value: '<b>val1</b>' } }
53
58
 
54
- it { is_expected.not_to be_html_safe }
59
+ it { is_expected.to eq(['<b>val1</b>']) }
60
+
61
+ it 'does not mark value as html_safe' do
62
+ expect(result.first).not_to be_html_safe
63
+ end
55
64
  end
56
65
 
57
66
  context 'when an explicit array value with unsafe characters is provided' do
58
67
  let(:options) { { value: ['<a', 'b'] } }
59
68
 
60
- it { is_expected.to eq('&lt;a and b').and be_html_safe }
69
+ it { is_expected.to eq(%w[<a b]) }
70
+
71
+ it 'does not mark values as html_safe' do
72
+ expect(subject.all?(&:html_safe?)).not_to be true
73
+ end
61
74
  end
62
75
 
63
76
  context 'when an explicit array value is provided' do
64
77
  let(:options) { { value: %w[a b c] } }
65
78
 
66
- it { is_expected.to eq 'a, b, and c' }
79
+ it { is_expected.to eq %w[a b c] }
67
80
  end
68
81
 
69
82
  context 'when an explicit value is provided' do
70
83
  let(:options) { { value: 'asdf' } }
71
84
 
72
- it { is_expected.to eq 'asdf' }
85
+ it { is_expected.to eq ['asdf'] }
73
86
  end
74
87
 
75
88
  context 'when field has a helper method' do
@@ -77,7 +90,7 @@ RSpec.describe Blacklight::FieldPresenter, :api do
77
90
  allow(request_context).to receive(:render_asdf_index_field).and_return('custom asdf value')
78
91
  end
79
92
 
80
- it { is_expected.to eq 'custom asdf value' }
93
+ it { is_expected.to eq ['custom asdf value'] }
81
94
  end
82
95
 
83
96
  context 'when field has link_to_facet with true' do
@@ -88,7 +101,7 @@ RSpec.describe Blacklight::FieldPresenter, :api do
88
101
 
89
102
  let(:field_name) { 'link_to_facet_true' }
90
103
 
91
- it { is_expected.to eq 'bar' }
104
+ it { is_expected.to eq ['bar'] }
92
105
  end
93
106
 
94
107
  context 'when field has link_to_facet with a field name' do
@@ -99,7 +112,7 @@ RSpec.describe Blacklight::FieldPresenter, :api do
99
112
 
100
113
  let(:field_name) { 'link_to_facet_named' }
101
114
 
102
- it { is_expected.to eq 'bar' }
115
+ it { is_expected.to eq ['bar'] }
103
116
  end
104
117
 
105
118
  context 'when no highlight field is available' do
@@ -120,14 +133,14 @@ RSpec.describe Blacklight::FieldPresenter, :api do
120
133
 
121
134
  let(:field_name) { 'highlight' }
122
135
 
123
- it { is_expected.to eq '<em>highlight</em>' }
136
+ it { is_expected.to eq ['<em>highlight</em>'] }
124
137
  end
125
138
 
126
139
  context 'when no options are provided' do
127
140
  let(:field_name) { 'qwer' }
128
141
 
129
142
  it "checks the document field value" do
130
- expect(subject).to eq 'document qwer value'
143
+ expect(subject).to eq ['document qwer value']
131
144
  end
132
145
  end
133
146
 
@@ -138,7 +151,7 @@ RSpec.describe Blacklight::FieldPresenter, :api do
138
151
 
139
152
  let(:field_name) { 'solr_doc_accessor' }
140
153
 
141
- it { is_expected.to eq '123' }
154
+ it { is_expected.to eq ['123'] }
142
155
  end
143
156
 
144
157
  context 'when accessor is set to a value' do
@@ -147,7 +160,7 @@ RSpec.describe Blacklight::FieldPresenter, :api do
147
160
  it 'calls the accessor with the field_name as the argument' do
148
161
  expect(document).to receive(:solr_doc_accessor).with('explicit_accessor').and_return("123")
149
162
 
150
- expect(subject).to eq '123'
163
+ expect(subject).to eq ['123']
151
164
  end
152
165
  end
153
166
 
@@ -157,7 +170,7 @@ RSpec.describe Blacklight::FieldPresenter, :api do
157
170
  it 'calls the accessors on the return of the preceeding' do
158
171
  allow(document).to receive_message_chain(:solr_doc_accessor, some_method: "123")
159
172
 
160
- expect(subject).to eq '123'
173
+ expect(subject).to eq ['123']
161
174
  end
162
175
  end
163
176
 
@@ -165,26 +178,26 @@ RSpec.describe Blacklight::FieldPresenter, :api do
165
178
  let(:field_name) { 'explicit_values_with_context' }
166
179
 
167
180
  it 'calls the accessors on the return of the preceeding' do
168
- expect(subject).to eq '1'
181
+ expect(subject).to eq ['1']
169
182
  end
170
183
  end
171
184
 
172
185
  context 'when the field is an alias' do
173
186
  let(:field_name) { 'alias' }
174
187
 
175
- it { is_expected.to eq 'document qwer value' }
188
+ it { is_expected.to eq ['document qwer value'] }
176
189
  end
177
190
 
178
191
  context 'when the field has a default' do
179
192
  let(:field_name) { 'with_default' }
180
193
 
181
- it { is_expected.to eq 'value' }
194
+ it { is_expected.to eq ['value'] }
182
195
  end
183
196
 
184
197
  context 'with steps' do
185
198
  let(:field_name) { 'with_steps' }
186
199
 
187
- it { is_expected.to eq 'Static step' }
200
+ it { is_expected.to eq ['Static step'] }
188
201
  end
189
202
 
190
203
  context 'for a field with the helper_method option' do
@@ -200,12 +213,80 @@ RSpec.describe Blacklight::FieldPresenter, :api do
200
213
  args.first
201
214
  end
202
215
 
203
- expect(result).to include :document, :field, :value, :config, :a
204
- expect(result[:document]).to eq document
205
- expect(result[:field]).to eq 'field_with_helper'
206
- expect(result[:value]).to eq ['value']
207
- expect(result[:config]).to eq field_config
208
- expect(result[:a]).to eq 1
216
+ expect(result).to contain_exactly({ document: document, field: field_name,
217
+ value: ['value'], config: field_config, a: 1 })
218
+ end
219
+ end
220
+
221
+ context 'when joining a field' do
222
+ context 'with join config' do
223
+ let(:field_name) { :join_true }
224
+
225
+ it { is_expected.to eq ['a, b, and c'] }
226
+
227
+ it 'marks the value as html_safe' do
228
+ expect(result.first).to be_html_safe
229
+ end
230
+ end
231
+
232
+ context 'with separator_options' do
233
+ let(:field_name) { :separator_options }
234
+
235
+ it { is_expected.to eq ['d<br/>e<br/>f'] }
236
+
237
+ it 'marks the value as html_safe' do
238
+ expect(result.first).to be_html_safe
239
+ end
240
+ end
241
+
242
+ context 'with join option' do
243
+ let(:options) { { join: true } }
244
+ let(:field_name) { 'some_field' }
245
+
246
+ it { is_expected.to eq ['1 and 2'] }
247
+
248
+ it 'marks the value as html_safe' do
249
+ expect(result.first).to be_html_safe
250
+ end
251
+ end
252
+ end
253
+
254
+ context 'with a single value' do
255
+ let(:options) { { join: true, value: 1 } }
256
+
257
+ it 'is not joined' do
258
+ expect(result).to eq [1]
259
+ end
260
+ end
261
+
262
+ context 'with a single html value' do
263
+ let(:options) { { join: true, value: '<b>value</b>' } }
264
+
265
+ it { is_expected.to eq ['<b>value</b>'] }
266
+
267
+ it 'does not mark the value as html_safe' do
268
+ expect(result.first).not_to be_html_safe
269
+ end
270
+ end
271
+
272
+ context 'when an array containing unsafe characters provided' do
273
+ let(:options) { { join: true, value: ['<a', 'b'] } }
274
+
275
+ it 'html escapes the unsafe characters' do
276
+ expect(result).to eq ["&lt;a and b"]
277
+ end
278
+
279
+ it 'marks the value as html_safe' do
280
+ expect(result.first).to be_html_safe
281
+ end
282
+ end
283
+
284
+ context 'when outside the html context' do
285
+ let(:field_name) { :join_true }
286
+ let(:options) { { format: 'json' } }
287
+
288
+ it 'does not join the values' do
289
+ expect(result).to eq %w[a b c]
209
290
  end
210
291
  end
211
292
  end