ckeditor5 1.15.9 → 1.16.0

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/lib/ckeditor5/rails/assets/webcomponents/components/context.mjs +23 -0
  4. data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +24 -0
  5. data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +33 -10
  6. data/lib/ckeditor5/rails/concerns/checksum.rb +15 -0
  7. data/lib/ckeditor5/rails/context/props.rb +17 -2
  8. data/lib/ckeditor5/rails/editor/props.rb +16 -3
  9. data/lib/ckeditor5/rails/editor/props_base_plugin.rb +19 -0
  10. data/lib/ckeditor5/rails/editor/props_inline_plugin.rb +6 -3
  11. data/lib/ckeditor5/rails/editor/props_plugin.rb +6 -4
  12. data/lib/ckeditor5/rails/engine.rb +3 -2
  13. data/lib/ckeditor5/rails/presets/plugins_builder.rb +9 -9
  14. data/lib/ckeditor5/rails/presets/preset_builder.rb +4 -6
  15. data/lib/ckeditor5/rails/version.rb +1 -1
  16. data/lib/ckeditor5/rails.rb +1 -0
  17. data/spec/e2e/features/editor_types_spec.rb +178 -0
  18. data/spec/e2e/features/form_integration_spec.rb +60 -0
  19. data/spec/e2e/spec_helper.rb +43 -0
  20. data/spec/e2e/support/eventually.rb +14 -0
  21. data/spec/e2e/support/form_helpers.rb +37 -0
  22. data/spec/lib/ckeditor5/rails/assets/asset_bundle_hml_serializer_spec.rb +7 -0
  23. data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +19 -1
  24. data/spec/lib/ckeditor5/rails/concerns/checksum_spec.rb +50 -0
  25. data/spec/lib/ckeditor5/rails/context/props_spec.rb +7 -1
  26. data/spec/lib/ckeditor5/rails/editor/props_base_plugin_spec.rb +27 -0
  27. data/spec/lib/ckeditor5/rails/editor/props_spec.rb +2 -0
  28. data/spec/lib/ckeditor5/rails/engine_spec.rb +88 -0
  29. data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +156 -0
  30. data/spec/lib/ckeditor5/rails/hooks/simple_form_spec.rb +100 -0
  31. data/spec/lib/ckeditor5/rails/presets/plugins_builder_spec.rb +10 -10
  32. data/spec/lib/ckeditor5/rails/presets/preset_builder_spec.rb +10 -0
  33. data/spec/spec_helper.rb +2 -2
  34. metadata +22 -2
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'e2e/spec_helper'
4
+
5
+ RSpec.describe 'Form Integration', type: :feature, js: true do
6
+ before do
7
+ visit('form')
8
+ setup_form_tracking(page)
9
+ end
10
+
11
+ shared_examples 'a form with CKEditor' do |form_testid, editor_testid, submit_testid| # rubocop:disable Metrics/BlockLength
12
+ let(:form) { find("[data-testid='#{form_testid}']") }
13
+ let(:editor) { find("[data-testid='#{editor_testid}']") }
14
+ let(:editable) { editor.find('.ck-editor__editable') }
15
+ let(:text_field) { editor.find('textarea', visible: :hidden) }
16
+ let(:submit_button) { find("[data-testid='#{submit_testid}']") }
17
+
18
+ it 'loads editor properly' do
19
+ expect(page).to have_css("[data-testid='#{editor_testid}'] .ck-editor__editable")
20
+ expect(editor).to have_invisible_textarea
21
+ end
22
+
23
+ it 'validates required fields' do
24
+ editable.click
25
+ editable.send_keys([[:control, 'a'], :backspace])
26
+
27
+ text_field.set('')
28
+ submit_button.click
29
+
30
+ expect(form).not_to have_been_submitted
31
+ expect(text_field).to be_invalid
32
+ end
33
+
34
+ it 'submits with valid data' do
35
+ editable.click
36
+ editable.send_keys('New content')
37
+ text_field.set('Second field value')
38
+
39
+ submit_button.click
40
+
41
+ eventually do
42
+ expect(form).to have_been_submitted
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'Rails form' do
48
+ it_behaves_like 'a form with CKEditor',
49
+ 'rails-form',
50
+ 'rails-form-editor',
51
+ 'rails-form-submit'
52
+ end
53
+
54
+ describe 'Simple form' do
55
+ it_behaves_like 'a form with CKEditor',
56
+ 'simple-form',
57
+ 'simple-form-editor',
58
+ 'simple-form-submit'
59
+ end
60
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara'
4
+ require 'capybara/rspec'
5
+ require 'capybara/cuprite'
6
+
7
+ ENV['RAILS_ENV'] ||= 'test'
8
+
9
+ require File.expand_path('../../sandbox/config/environment', __dir__)
10
+
11
+ require 'capybara/rails'
12
+
13
+ Capybara.app = Rails.application
14
+
15
+ Capybara.register_driver(:cuprite) do |app|
16
+ driver = Capybara::Cuprite::Driver.new(
17
+ app,
18
+ window_size: [1200, 800],
19
+ headless: ENV['HEADLESS'] == 'true',
20
+ browser_options: {
21
+ 'no-sandbox': nil,
22
+ 'disable-gpu': nil,
23
+ 'enable-logging': nil
24
+ },
25
+ process_timeout: 20,
26
+ timeout: 20,
27
+ inspector: true
28
+ )
29
+
30
+ process = driver.browser.process
31
+ puts ''
32
+ puts "Browser: #{process.browser_version}"
33
+ puts "Protocol: #{process.protocol_version}"
34
+ puts "V8: #{process.v8_version}"
35
+ puts "Webkit: #{process.webkit_version}"
36
+ driver
37
+ end
38
+
39
+ Capybara.server = :webrick
40
+ Capybara.default_driver = :cuprite
41
+ Capybara.javascript_driver = :cuprite
42
+
43
+ Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add eventually helper for async operations
4
+ def eventually(timeout: 5, delay: 0.1)
5
+ deadline = Time.zone.now + timeout
6
+ loop do
7
+ yield
8
+ break
9
+ rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e
10
+ raise e if Time.zone.now >= deadline
11
+
12
+ sleep delay
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FormHelpers
4
+ def setup_form_tracking(driver)
5
+ driver.execute_script <<~JS
6
+ window.lastSubmittedForm = null;
7
+
8
+ document.addEventListener('submit', (e) => {
9
+ e.preventDefault();
10
+ window.lastSubmittedForm = e.target.id;
11
+ });
12
+ JS
13
+ end
14
+ end
15
+
16
+ RSpec.configure do |config|
17
+ config.include FormHelpers, type: :feature
18
+ end
19
+
20
+ RSpec::Matchers.define :be_invalid do
21
+ match do |element|
22
+ element[:validity] == 'false' ||
23
+ element.evaluate_script('!this.validity.valid')
24
+ end
25
+ end
26
+
27
+ RSpec::Matchers.define :have_been_submitted do
28
+ match do |form|
29
+ page.evaluate_script('window.lastSubmittedForm') == form['id']
30
+ end
31
+ end
32
+
33
+ RSpec::Matchers.define :have_invisible_textarea do
34
+ match do |element|
35
+ element.has_css?('textarea', visible: :hidden)
36
+ end
37
+ end
@@ -86,6 +86,13 @@ RSpec.describe CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer do
86
86
  it 'includes web component script' do
87
87
  expect(html).to include('<script type="module" nonce="true">')
88
88
  end
89
+
90
+ it 'memoizes scripts import map' do
91
+ first_call = serializer.send(:scripts_import_map_tag)
92
+ second_call = serializer.send(:scripts_import_map_tag)
93
+
94
+ expect(first_call.object_id).to eq(second_call.object_id)
95
+ end
89
96
  end
90
97
 
91
98
  describe '.url_resource_preload_type' do
@@ -160,7 +160,25 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
160
160
  )
161
161
  end
162
162
 
163
- it 'raises error when version is missing' do
163
+ before do
164
+ allow(helper).to receive(:merge_with_editor_preset).and_return({})
165
+ end
166
+
167
+ it 'raises error about missing required parameters' do
168
+ expect { helper.ckeditor5_assets(preset: :default) }
169
+ .to raise_error(NoMatchingPatternKeyError)
170
+ end
171
+ end
172
+
173
+ context 'with empty hash from preset' do
174
+ let(:preset) do
175
+ instance_double(
176
+ CKEditor5::Rails::Presets::PresetBuilder,
177
+ to_h_with_overrides: {}
178
+ )
179
+ end
180
+
181
+ it 'raises error about missing version and type' do
164
182
  expect { helper.ckeditor5_assets(preset: :default) }
165
183
  .to raise_error(ArgumentError, /forgot to define version/)
166
184
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Concerns::Checksum do
6
+ let(:dummy_class) do
7
+ Class.new do
8
+ include CKEditor5::Rails::Concerns::Checksum
9
+
10
+ public :calculate_object_checksum
11
+ end
12
+ end
13
+
14
+ subject(:instance) { dummy_class.new }
15
+
16
+ describe '#calculate_object_checksum' do
17
+ it 'returns a 16-character string' do
18
+ result = instance.calculate_object_checksum({ test: 'value' })
19
+ expect(result).to eq(
20
+ 'f98be16ebfa861cb39a61faff9e52b33f5bcc16bb6ae72e728d226dc07093932'
21
+ )
22
+ end
23
+
24
+ it 'returns consistent checksums for the same input' do
25
+ input = { name: 'test', value: 123 }
26
+ first_result = instance.calculate_object_checksum(input)
27
+ second_result = instance.calculate_object_checksum(input)
28
+ expect(first_result).to eq(second_result)
29
+ end
30
+
31
+ it 'returns different checksums for different inputs' do
32
+ result1 = instance.calculate_object_checksum({ a: 1 })
33
+ result2 = instance.calculate_object_checksum({ a: 2 })
34
+ expect(result1).not_to eq(result2)
35
+ end
36
+
37
+ it 'handles arrays' do
38
+ result = instance.calculate_object_checksum([1, 2, 3])
39
+ expect(result).to eq(
40
+ 'a615eeaee21de5179de080de8c3052c8da901138406ba71c38c032845f7d54f4'
41
+ )
42
+ end
43
+
44
+ it 'is order dependent for hashes' do
45
+ result1 = instance.calculate_object_checksum({ a: 1, b: 2 })
46
+ result2 = instance.calculate_object_checksum({ b: 2, a: 1 })
47
+ expect(result1).not_to eq(result2)
48
+ end
49
+ end
50
+ end
@@ -25,9 +25,15 @@ RSpec.describe CKEditor5::Rails::Context::Props do
25
25
  describe '#to_attributes' do
26
26
  subject(:attributes) { props.to_attributes }
27
27
 
28
+ it 'returns integrity property' do
29
+ expect(attributes[:integrity]).to eq(
30
+ '24e46c3ee19f6764930b38ecdf62c0ac824a0acbe6616b46199d892afb211acb'
31
+ )
32
+ end
33
+
28
34
  it 'returns a hash with plugins and config keys' do
29
35
  expect(attributes).to be_a(Hash)
30
- expect(attributes.keys).to match_array(%i[plugins config])
36
+ expect(attributes.keys).to match_array(%i[plugins integrity config])
31
37
  end
32
38
 
33
39
  describe ':plugins key' do
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Editor::PropsBasePlugin do
6
+ let(:concrete_class) do
7
+ Class.new(described_class) do
8
+ def to_unsafe_h
9
+ { type: :test, name: name }
10
+ end
11
+ end
12
+ end
13
+
14
+ let(:instance) { concrete_class.new(:TestPlugin) }
15
+
16
+ describe '#initialize' do
17
+ it 'sets the name attribute' do
18
+ expect(instance.name).to eq(:TestPlugin)
19
+ end
20
+ end
21
+
22
+ describe '#to_h' do
23
+ it 'raises NotImplementedError' do
24
+ expect { instance.to_h }.to raise_error(NotImplementedError)
25
+ end
26
+ end
27
+ end
@@ -28,11 +28,13 @@ RSpec.describe CKEditor5::Rails::Editor::Props do
28
28
 
29
29
  it 'includes required attributes' do
30
30
  attributes = props.to_attributes
31
+
31
32
  expect(attributes).to include(
32
33
  type: 'ClassicEditor',
33
34
  translations: String,
34
35
  plugins: String,
35
36
  config: String,
37
+ integrity: '358d88b83d041f208d94ac957b2fd68135f1caab5c0d101d33cf04d5d39d81ef',
36
38
  watchdog: true
37
39
  )
38
40
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Engine do
6
+ describe 'configuration' do
7
+ let(:preset) { instance_double(CKEditor5::Rails::Presets::PresetBuilder) }
8
+ let(:preset_manager) { instance_double(CKEditor5::Rails::Presets::Manager) }
9
+
10
+ before do
11
+ allow(described_class).to receive(:base).and_return(ActiveSupport::OrderedOptions.new)
12
+ allow(described_class.base).to receive(:presets).and_return(preset_manager)
13
+ allow(preset_manager).to receive(:default).and_return(preset)
14
+ end
15
+
16
+ it 'has default configuration' do
17
+ expect(described_class.base).to be_a(ActiveSupport::OrderedOptions)
18
+
19
+ default_preset = described_class.default_preset
20
+
21
+ expect(default_preset.type).to eq(:classic)
22
+ expect(default_preset.toolbar.items).to include(:undo, :redo, :'|', :heading)
23
+ expect(default_preset.plugins.items.map(&:name)).to include(:Essentials, :Paragraph, :Heading)
24
+ end
25
+
26
+ describe '.configure' do
27
+ it 'yields configuration proxy' do
28
+ yielded_config = nil
29
+ described_class.configure do |config|
30
+ yielded_config = config
31
+ end
32
+ expect(yielded_config).to be_a(described_class::ConfigurationProxy)
33
+ end
34
+
35
+ it 'allows configuring default preset' do
36
+ described_class.configure do
37
+ automatic_upgrades enabled: false
38
+ version '35.0.0'
39
+ license_key '1234567'
40
+ end
41
+
42
+ expect(described_class.default_preset.version).to eq('35.0.0')
43
+ expect(described_class.default_preset.license_key).to eq('1234567')
44
+ end
45
+ end
46
+
47
+ describe '.find_preset' do
48
+ before do
49
+ allow(preset_manager).to receive(:[]).with(:custom).and_return(preset)
50
+ allow(preset_manager).to receive(:[]).with(
51
+ kind_of(CKEditor5::Rails::Presets::PresetBuilder)
52
+ ).and_return(preset)
53
+ end
54
+
55
+ it 'returns preset instance if provided' do
56
+ test_preset = CKEditor5::Rails::Presets::PresetBuilder.new
57
+ expect(described_class.find_preset(test_preset)).to eq(test_preset)
58
+ end
59
+
60
+ it 'looks up preset by name' do
61
+ expect(described_class.find_preset(:custom)).to eq(preset)
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'initializers' do
67
+ describe 'helper initializer' do
68
+ it 'includes helpers in ActionView and ActionController' do
69
+ expect(ActionView::Base.included_modules).to include(CKEditor5::Rails::Helpers)
70
+ expect(ActionController::Base.included_modules).to include(CKEditor5::Rails::Helpers)
71
+ end
72
+ end
73
+
74
+ describe 'form_builder initializer' do
75
+ it 'includes FormBuilderExtension in ActionView::Helpers::FormBuilder' do
76
+ expect(ActionView::Helpers::FormBuilder.included_modules)
77
+ .to include(CKEditor5::Rails::Hooks::Form::FormBuilderExtension)
78
+ end
79
+ end
80
+
81
+ describe 'simple_form initializer', if: defined?(SimpleForm) do
82
+ it 'registers ckeditor5 input type' do
83
+ expect(SimpleForm::FormBuilder.mappings[:ckeditor5])
84
+ .to eq(CKEditor5::Rails::Hooks::SimpleForm::CKEditor5Input)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -42,6 +42,162 @@ RSpec.describe CKEditor5::Rails::Hooks::Form do
42
42
  })
43
43
  end
44
44
  end
45
+
46
+ context 'with as parameter' do
47
+ subject(:rendered_editor) do
48
+ builder.build_editor(:content, as: 'custom_field')
49
+ end
50
+
51
+ it 'uses custom field name' do
52
+ expect(rendered_editor).to have_tag('ckeditor-component', with: {
53
+ name: 'post[custom_field]'
54
+ })
55
+ end
56
+ end
57
+
58
+ context 'without object_name' do
59
+ let(:builder) { described_class.new('', post, template) }
60
+
61
+ it 'uses method name as field name' do
62
+ expect(rendered_editor).to have_tag('ckeditor-component', with: {
63
+ name: 'content'
64
+ })
65
+ end
66
+
67
+ context 'with as parameter' do
68
+ subject(:rendered_editor) do
69
+ builder.build_editor(:content, as: 'custom_field')
70
+ end
71
+
72
+ it 'uses as parameter as field name' do
73
+ expect(rendered_editor).to have_tag('ckeditor-component', with: {
74
+ name: 'custom_field'
75
+ })
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'with initial data handling' do
81
+ before do
82
+ allow(template).to receive(:ckeditor5_editor)
83
+ end
84
+
85
+ context 'when object responds to the method' do
86
+ it 'passes object method value as initial_data' do
87
+ builder.build_editor(:content)
88
+
89
+ expect(template).to have_received(:ckeditor5_editor)
90
+ .with(hash_including(initial_data: 'Initial content'))
91
+ end
92
+ end
93
+
94
+ context 'when object does not respond to the method' do
95
+ let(:post) { double('Post') }
96
+
97
+ it 'passes options initial_data value' do
98
+ builder.build_editor(:content, initial_data: 'Provided content')
99
+
100
+ expect(template).to have_received(:ckeditor5_editor)
101
+ .with(hash_including(initial_data: 'Provided content'))
102
+ end
103
+ end
104
+ end
105
+
106
+ context 'with validation classes handling' do
107
+ before do
108
+ allow(template).to receive(:ckeditor5_editor)
109
+ end
110
+
111
+ context 'when object has errors on the field' do
112
+ let(:post) do
113
+ instance_double('Post', errors: { content: ['is invalid'] })
114
+ end
115
+
116
+ it 'adds is-invalid class' do
117
+ builder.build_editor(:content, class: 'custom-class')
118
+
119
+ expect(template).to have_received(:ckeditor5_editor)
120
+ .with(hash_including(class: 'custom-class is-invalid'))
121
+ end
122
+
123
+ it 'adds is-invalid class when no initial class exists' do
124
+ builder.build_editor(:content)
125
+
126
+ expect(template).to have_received(:ckeditor5_editor)
127
+ .with(hash_including(class: 'is-invalid'))
128
+ end
129
+ end
130
+
131
+ context 'when object has no errors' do
132
+ let(:post) do
133
+ instance_double('Post', errors: {})
134
+ end
135
+
136
+ it 'keeps original class unchanged' do
137
+ builder.build_editor(:content, class: 'custom-class')
138
+
139
+ expect(template).to have_received(:ckeditor5_editor)
140
+ .with(hash_including(class: 'custom-class'))
141
+ end
142
+ end
143
+
144
+ context 'when object does not respond to errors' do
145
+ let(:post) { double('Post') }
146
+
147
+ it 'keeps original class unchanged' do
148
+ builder.build_editor(:content, class: 'custom-class')
149
+
150
+ expect(template).to have_received(:ckeditor5_editor)
151
+ .with(hash_including(class: 'custom-class'))
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ describe CKEditor5::Rails::Hooks::Form::FormBuilderExtension do
159
+ let(:template) { instance_double('ActionView::Base') }
160
+ let(:object) { double('Post') }
161
+ let(:builder) do
162
+ Class.new do
163
+ include CKEditor5::Rails::Hooks::Form::FormBuilderExtension
164
+ attr_reader :object_name, :object, :template
165
+
166
+ def initialize(object_name, object, template)
167
+ @object_name = object_name
168
+ @object = object
169
+ @template = template
170
+ end
171
+ end.new('post', object, template)
172
+ end
173
+
174
+ describe '#ckeditor5' do
175
+ let(:input_builder) { instance_double(CKEditor5::Rails::Hooks::Form::EditorInputBuilder) }
176
+
177
+ before do
178
+ allow(CKEditor5::Rails::Hooks::Form::EditorInputBuilder)
179
+ .to receive(:new)
180
+ .with('post', object, template)
181
+ .and_return(input_builder)
182
+ allow(input_builder).to receive(:build_editor)
183
+ end
184
+
185
+ it 'creates EditorInputBuilder with correct parameters' do
186
+ builder.ckeditor5(:content, class: 'custom-class')
187
+
188
+ expect(CKEditor5::Rails::Hooks::Form::EditorInputBuilder)
189
+ .to have_received(:new)
190
+ .with('post', object, template)
191
+ end
192
+
193
+ it 'calls build_editor with correct parameters' do
194
+ options = { class: 'custom-class' }
195
+ builder.ckeditor5(:content, options)
196
+
197
+ expect(input_builder)
198
+ .to have_received(:build_editor)
199
+ .with(:content, options)
200
+ end
45
201
  end
46
202
  end
47
203
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Hooks::SimpleForm::CKEditor5Input do
6
+ let(:template) { instance_double('ActionView::Base') }
7
+ let(:object) { double('Post', content: 'Initial content') }
8
+ let(:object_name) { 'post' }
9
+ let(:builder) do
10
+ double('FormBuilder',
11
+ object: object,
12
+ object_name: object_name,
13
+ template: template)
14
+ end
15
+ let(:attribute_name) { :content }
16
+ let(:input_html_options) { {} }
17
+ let(:input_options) { {} }
18
+
19
+ subject(:input) do
20
+ described_class.new(builder, attribute_name, nil, input_html_options, input_options)
21
+ end
22
+
23
+ before do
24
+ allow(template).to receive(:ckeditor5_editor)
25
+ end
26
+
27
+ describe '#input' do
28
+ it 'renders ckeditor with default options' do
29
+ input.input
30
+
31
+ expect(template).to have_received(:ckeditor5_editor).with(
32
+ hash_including(
33
+ preset: :default,
34
+ type: :classic,
35
+ config: nil,
36
+ initial_data: 'Initial content',
37
+ name: 'post[content]',
38
+ class: [{}, :required] # Simple Form adds these classes by default
39
+ )
40
+ )
41
+ end
42
+
43
+ context 'with custom options' do
44
+ let(:input_options) do
45
+ {
46
+ preset: :custom_preset,
47
+ type: :inline,
48
+ config: { toolbar: [:bold] }
49
+ }
50
+ end
51
+ let(:input_html_options) do
52
+ { class: 'custom-class', id: 'custom-id' }
53
+ end
54
+
55
+ it 'renders ckeditor with merged options' do
56
+ input.input
57
+
58
+ expect(template).to have_received(:ckeditor5_editor).with(
59
+ hash_including(
60
+ preset: :custom_preset,
61
+ type: :inline,
62
+ config: { toolbar: [:bold] },
63
+ initial_data: 'Initial content',
64
+ name: 'post[content]',
65
+ class: [{ class: 'custom-class', id: 'custom-id' }, :required]
66
+ )
67
+ )
68
+ end
69
+ end
70
+
71
+ context 'when object does not respond to attribute' do
72
+ let(:object) { double('Post') }
73
+ let(:input_options) { { initial_data: 'Provided content' } }
74
+
75
+ it 'uses initial_data from options' do
76
+ input.input
77
+
78
+ expect(template).to have_received(:ckeditor5_editor).with(
79
+ hash_including(initial_data: 'Provided content')
80
+ )
81
+ end
82
+ end
83
+
84
+ context 'with wrapper options' do
85
+ let(:wrapper_options) { { wrapper_class: 'wrapper' } }
86
+ let(:input_html_options) { { class: 'input' } }
87
+
88
+ it 'merges wrapper options with input options' do
89
+ input.input(wrapper_options)
90
+
91
+ expect(template).to have_received(:ckeditor5_editor).with(
92
+ hash_including(
93
+ class: [{ class: 'input' }, :required],
94
+ wrapper_class: 'wrapper'
95
+ )
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end