ckeditor5 1.20.1 → 1.22.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -2
  3. data/lib/ckeditor5/rails/assets/assets_bundle.rb +11 -1
  4. data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +5 -10
  5. data/lib/ckeditor5/rails/assets/webcomponent_bundle.rb +10 -3
  6. data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +35 -2
  7. data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +36 -2
  8. data/lib/ckeditor5/rails/cdn/concerns/bundle_builder.rb +57 -0
  9. data/lib/ckeditor5/rails/cdn/helpers.rb +62 -55
  10. data/lib/ckeditor5/rails/editor/helpers/editor_helpers.rb +25 -19
  11. data/lib/ckeditor5/rails/editor/props.rb +7 -14
  12. data/lib/ckeditor5/rails/engine.rb +13 -0
  13. data/lib/ckeditor5/rails/presets/manager.rb +2 -0
  14. data/lib/ckeditor5/rails/presets/preset_builder.rb +1 -1
  15. data/lib/ckeditor5/rails/presets/toolbar_builder.rb +117 -3
  16. data/lib/ckeditor5/rails/version.rb +1 -1
  17. data/spec/e2e/features/ajax_form_integration_spec.rb +78 -0
  18. data/spec/e2e/features/lazy_assets_spec.rb +54 -0
  19. data/spec/e2e/support/form_helpers.rb +4 -1
  20. data/spec/lib/ckeditor5/rails/assets/assets_bundle_hml_serializer_spec.rb +53 -0
  21. data/spec/lib/ckeditor5/rails/assets/assets_bundle_spec.rb +2 -1
  22. data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +95 -3
  23. data/spec/lib/ckeditor5/rails/editor/helpers/editor_helpers_spec.rb +85 -25
  24. data/spec/lib/ckeditor5/rails/editor/props_spec.rb +14 -34
  25. data/spec/lib/ckeditor5/rails/engine_spec.rb +10 -0
  26. data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +15 -2
  27. data/spec/lib/ckeditor5/rails/plugins/wproofreader_spec.rb +2 -0
  28. data/spec/lib/ckeditor5/rails/presets/toolbar_builder_spec.rb +119 -0
  29. data/spec/lib/ckeditor5/rails/version_detector_spec.rb +1 -1
  30. metadata +7 -2
@@ -14,16 +14,17 @@ module CKEditor5::Rails::Editor
14
14
  }.freeze
15
15
 
16
16
  def initialize(
17
- controller_context, type, config,
18
- watchdog: true, editable_height: nil, language: nil
17
+ type, config,
18
+ bundle: nil,
19
+ watchdog: true,
20
+ editable_height: nil
19
21
  )
20
22
  raise ArgumentError, "Invalid editor type: #{type}" unless Props.valid_editor_type?(type)
21
23
 
22
- @controller_context = controller_context
24
+ @bundle = bundle
23
25
  @watchdog = watchdog
24
26
  @type = type
25
27
  @config = config
26
- @language = language
27
28
  @editable_height = EditableHeightNormalizer.new(type).normalize(editable_height)
28
29
  end
29
30
 
@@ -40,11 +41,11 @@ module CKEditor5::Rails::Editor
40
41
 
41
42
  private
42
43
 
43
- attr_reader :controller_context, :watchdog, :type, :config, :editable_height
44
+ attr_reader :bundle, :watchdog, :type, :config, :editable_height
44
45
 
45
46
  def serialized_attributes
46
47
  {
47
- translations: serialize_translations,
48
+ bundle: bundle.to_json,
48
49
  plugins: serialize_plugins,
49
50
  config: serialize_config,
50
51
  watchdog: watchdog
@@ -52,10 +53,6 @@ module CKEditor5::Rails::Editor
52
53
  .merge(editable_height ? { 'editable-height' => editable_height } : {})
53
54
  end
54
55
 
55
- def serialize_translations
56
- controller_context[:bundle]&.translations_scripts&.map(&:to_h).to_json || '[]'
57
- end
58
-
59
56
  def serialize_plugins
60
57
  (config[:plugins] || []).map { |plugin| PropsBasePlugin.normalize(plugin).to_h }.to_json
61
58
  end
@@ -63,10 +60,6 @@ module CKEditor5::Rails::Editor
63
60
  def serialize_config
64
61
  config
65
62
  .except(:plugins)
66
- .tap do |cfg|
67
- cfg[:licenseKey] = controller_context[:license_key] if controller_context[:license_key]
68
- cfg[:language] = { ui: @language } if @language
69
- end
70
63
  .to_json
71
64
  end
72
65
  end
@@ -7,6 +7,8 @@ require_relative 'plugins/simple_upload_adapter'
7
7
  require_relative 'plugins/wproofreader'
8
8
 
9
9
  module CKEditor5::Rails
10
+ class PresetNotFoundError < ArgumentError; end
11
+
10
12
  class Engine < ::Rails::Engine
11
13
  config.ckeditor5 = ActiveSupport::OrderedOptions.new
12
14
  config.ckeditor5.presets = Presets::Manager.new
@@ -47,6 +49,10 @@ module CKEditor5::Rails
47
49
  config.ckeditor5.presets.default
48
50
  end
49
51
 
52
+ def presets
53
+ config.ckeditor5.presets
54
+ end
55
+
50
56
  def configure(&block)
51
57
  proxy = ConfigurationProxy.new(config.ckeditor5)
52
58
  proxy.instance_eval(&block)
@@ -57,6 +63,13 @@ module CKEditor5::Rails
57
63
 
58
64
  base.presets[preset]
59
65
  end
66
+
67
+ def find_preset!(preset)
68
+ found_preset = find_preset(preset)
69
+ return found_preset if found_preset.present?
70
+
71
+ raise PresetNotFoundError, "Preset '#{preset}' not found. Please define it in the initializer."
72
+ end
60
73
  end
61
74
 
62
75
  class ConfigurationProxy
@@ -8,6 +8,8 @@ module CKEditor5::Rails::Presets
8
8
  class Manager
9
9
  attr_reader :presets
10
10
 
11
+ alias to_h presets
12
+
11
13
  # Initializes a new Manager instance and sets up the default preset
12
14
  def initialize
13
15
  @presets = {}
@@ -86,7 +86,7 @@ module CKEditor5::Rails
86
86
  # Merge preset with configuration hash
87
87
  # @param overrides [Hash] Configuration options to merge
88
88
  # @return [self]
89
- def merge_with_hash!(**overrides) # rubocop:disable Metrics/AbcSize
89
+ def merge_with_hash!(**overrides)
90
90
  @version = Semver.new(overrides[:version]) if overrides.key?(:version)
91
91
  @premium = overrides.fetch(:premium, premium)
92
92
  @cdn = overrides.fetch(:cdn, cdn)
@@ -1,13 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CKEditor5::Rails::Presets
4
+ # Builder class for configuring CKEditor5 toolbar items.
5
+ #
6
+ # @example Basic toolbar configuration
7
+ # toolbar = ToolbarBuilder.new([:bold, :italic])
8
+ # toolbar.append(:link)
9
+ # toolbar.prepend(:heading)
4
10
  class ToolbarBuilder
5
11
  attr_reader :items
6
12
 
13
+ # Initialize a new toolbar builder with given items.
14
+ #
15
+ # @param items [Array<Symbol>] Initial toolbar items
16
+ # @example Create new toolbar
17
+ # ToolbarBuilder.new([:bold, :italic, :|, :link])
7
18
  def initialize(items)
8
19
  @items = items
9
20
  end
10
21
 
22
+ # Returns toolbar line break symbol
23
+ #
24
+ # @return [Symbol] Line break symbol (-)
25
+ # @example Add line break to toolbar
26
+ # toolbar do
27
+ # append :bold, break_line, :italic
28
+ # end
29
+ def break_line
30
+ :-
31
+ end
32
+
33
+ # Returns toolbar separator symbol
34
+ #
35
+ # @return [Symbol] Separator symbol (|)
36
+ # @example Add separator to toolbar
37
+ # toolbar do
38
+ # append :bold, separator, :italic
39
+ # end
40
+ def separator
41
+ :|
42
+ end
43
+
11
44
  # Remove items from the editor toolbar.
12
45
  #
13
46
  # @param removed_items [Array<Symbol>] Toolbar items to be removed
@@ -16,7 +49,9 @@ module CKEditor5::Rails::Presets
16
49
  # remove :underline, :heading
17
50
  # end
18
51
  def remove(*removed_items)
19
- removed_items.each { |item| items.delete(item) }
52
+ items.delete_if do |existing_item|
53
+ removed_items.any? { |item_to_remove| item_matches?(existing_item, item_to_remove) }
54
+ end
20
55
  end
21
56
 
22
57
  # Prepend items to the editor toolbar.
@@ -34,7 +69,7 @@ module CKEditor5::Rails::Presets
34
69
  # @raise [ArgumentError] When the specified 'before' item is not found
35
70
  def prepend(*prepended_items, before: nil)
36
71
  if before
37
- index = items.index(before)
72
+ index = find_item_index(before)
38
73
  raise ArgumentError, "Item '#{before}' not found in array" unless index
39
74
 
40
75
  items.insert(index, *prepended_items)
@@ -58,7 +93,7 @@ module CKEditor5::Rails::Presets
58
93
  # @raise [ArgumentError] When the specified 'after' item is not found
59
94
  def append(*appended_items, after: nil)
60
95
  if after
61
- index = items.index(after)
96
+ index = find_item_index(after)
62
97
  raise ArgumentError, "Item '#{after}' not found in array" unless index
63
98
 
64
99
  items.insert(index + 1, *appended_items)
@@ -66,5 +101,84 @@ module CKEditor5::Rails::Presets
66
101
  items.push(*appended_items)
67
102
  end
68
103
  end
104
+
105
+ # Find group by name in toolbar items
106
+ #
107
+ # @param name [Symbol] Group name to find
108
+ # @return [ToolbarGroupItem, nil] Found group or nil
109
+ def find_group(name)
110
+ items.find { |item| item.is_a?(ToolbarGroupItem) && item.name == name }
111
+ end
112
+
113
+ # Remove group by name from toolbar items
114
+ #
115
+ # @param name [Symbol] Group name to remove
116
+ def remove_group(name)
117
+ items.delete_if { |item| item.is_a?(ToolbarGroupItem) && item.name == name }
118
+ end
119
+
120
+ # Create and add new group to toolbar
121
+ #
122
+ # @param name [Symbol] Group name
123
+ # @param options [Hash] Group options (label:, icons:)
124
+ # @param block [Proc] Configuration block
125
+ # @return [ToolbarGroupItem] Created group
126
+ def group(name, **options, &block)
127
+ group = ToolbarGroupItem.new(name, [], **options)
128
+ group.instance_eval(&block) if block_given?
129
+ items << group
130
+ group
131
+ end
132
+
133
+ private
134
+
135
+ # Find index of an item or group by name
136
+ #
137
+ # @param item [Symbol] Item or group name to find
138
+ # @return [Integer, nil] Index of the found item or nil
139
+ def find_item_index(item)
140
+ items.find_index { |existing_item| item_matches?(existing_item, item) }
141
+ end
142
+
143
+ # Checks if the existing item matches the given item or group name
144
+ #
145
+ # @param existing_item [Symbol, ToolbarGroupItem] Item to check
146
+ # @param item [Symbol] Item or group name to match against
147
+ # @return [Boolean] true if items match, false otherwise
148
+ # @example Check if items match
149
+ # item_matches?(:bold, :bold) # => true
150
+ # item_matches?(group(:text), :text) # => true
151
+ def item_matches?(existing_item, item)
152
+ if existing_item.is_a?(ToolbarGroupItem)
153
+ existing_item.name == item
154
+ else
155
+ existing_item == item
156
+ end
157
+ end
158
+ end
159
+
160
+ # Builder class for configuring CKEditor5 toolbar groups.
161
+ # Allows creating named groups of toolbar items with optional labels and icons.
162
+ #
163
+ # @example Creating a text formatting group
164
+ # group = ToolbarGroupItem.new(:text_formatting, [:bold, :italic], label: 'Text')
165
+ # group.append(:underline)
166
+ class ToolbarGroupItem < ToolbarBuilder
167
+ attr_reader :name, :label, :icon
168
+
169
+ # Initialize a new toolbar group item.
170
+ #
171
+ # @param name [Symbol] Name of the toolbar group
172
+ # @param items [Array<Symbol>, ToolbarBuilder] Items to be included in the group
173
+ # @param label [String, nil] Optional label for the group
174
+ # @param icon [String, nil] Optional icon for the group
175
+ # @example Create a new toolbar group
176
+ # ToolbarGroupItem.new(:text, [:bold, :italic], label: 'Text formatting')
177
+ def initialize(name, items = [], label: nil, icon: nil)
178
+ super(items)
179
+ @name = name
180
+ @label = label
181
+ @icon = icon
182
+ end
69
183
  end
70
184
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CKEditor5
4
4
  module Rails
5
- VERSION = '1.20.1'
5
+ VERSION = '1.22.0'
6
6
 
7
7
  DEFAULT_CKEDITOR_VERSION = '44.0.0'
8
8
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'e2e/spec_helper'
4
+
5
+ RSpec.describe 'AJAX Form Integration', type: :feature, js: true do
6
+ before do
7
+ visit('form_ajax')
8
+ setup_form_tracking(page)
9
+ end
10
+
11
+ shared_examples 'an ajax form with CKEditor' do |form_testid, editor_testid, submit_testid, response_id| # 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
+ let(:response_container) { find("##{response_id}") }
18
+
19
+ before do
20
+ expect(page).to have_css('.ck-editor__editable', wait: 10)
21
+ end
22
+
23
+ it 'loads editor properly' do
24
+ expect(page).to have_css("[data-testid='#{editor_testid}'] .ck-editor__editable")
25
+ expect(editor).to have_invisible_textarea
26
+ end
27
+
28
+ it 'validates required fields' do
29
+ editable.click
30
+ editable.send_keys([[:control, 'a'], :backspace])
31
+ text_field.set('')
32
+ submit_button.click
33
+
34
+ expect(form).not_to have_been_submitted
35
+ expect(text_field).to be_invalid
36
+ end
37
+
38
+ it 'submits form and shows response' do
39
+ test_content = "Test content #{Time.now.to_i}"
40
+
41
+ editable.click
42
+ editable.send_keys([[:control, 'a'], :backspace])
43
+ editable.send_keys(test_content)
44
+
45
+ sleep 1
46
+
47
+ submit_button.click
48
+
49
+ eventually(timeout: 13) do
50
+ expect(response_container).to be_visible
51
+ expect(response_container).to have_text('Success!')
52
+ expect(response_container).to have_text(test_content)
53
+
54
+ # Verify that CKEditor initializes in the response
55
+ response_editor = response_container.find('.ck-editor__editable', wait: 10)
56
+
57
+ expect(response_editor).to be_visible
58
+ expect(response_editor).to have_text(test_content)
59
+ end
60
+ end
61
+ end
62
+
63
+ describe 'Regular AJAX form' do
64
+ it_behaves_like 'an ajax form with CKEditor',
65
+ 'rails-form',
66
+ 'rails-form-editor',
67
+ 'rails-form-submit',
68
+ 'response'
69
+ end
70
+
71
+ describe 'Turbo Stream form' do
72
+ it_behaves_like 'an ajax form with CKEditor',
73
+ 'rails-form-stream',
74
+ 'rails-form-editor-stream',
75
+ 'rails-form-submit-stream',
76
+ 'response-stream'
77
+ end
78
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'e2e/spec_helper'
4
+
5
+ RSpec.describe 'Lazy Assets', type: :feature do
6
+ context 'without JavaScript', js: false do
7
+ before do
8
+ visit 'classic_lazy_assets?no_embed=true'
9
+ end
10
+
11
+ it 'does not load CKEditor assets' do
12
+ expect(page).not_to have_css('link[href*="ckeditor5"]', visible: false)
13
+ expect(page).not_to have_css('script[src*="ckeditor5"]', visible: false)
14
+
15
+ scripts = page.all('script:not([type="importmap"]):not([type="module"])', visible: false)
16
+ external_scripts = scripts.reject { |s| s[:src].nil? }
17
+ expect(external_scripts).not_to include(match(/ckeditor5/))
18
+
19
+ expect(page).to have_css('script[type="importmap"]', visible: false)
20
+ end
21
+ end
22
+
23
+ context 'with JavaScript', js: true do
24
+ before { visit 'classic_lazy_assets' }
25
+
26
+ it 'loads editor when needed' do
27
+ expect(page).to have_css('.ck-editor__editable', wait: 10)
28
+ expect(page).to have_css('link[href*="ckeditor5"]', visible: false)
29
+ end
30
+
31
+ it 'initializes editor properly' do
32
+ editor = find('.ck-editor__editable')
33
+ expect(editor).to be_visible
34
+
35
+ editor.click
36
+ editor.send_keys('Test content')
37
+
38
+ expect(editor).to have_text('Test content')
39
+ end
40
+
41
+ it 'supports multiple editor instances' do
42
+ visit 'classic_lazy_assets?multiple=true'
43
+
44
+ editors = all('.ck-editor__editable', wait: 10)
45
+ expect(editors.length).to eq(2)
46
+
47
+ editors.each do |editor|
48
+ editor.click
49
+ editor.send_keys('Content for editor')
50
+ expect(editor).to have_text('Content for editor')
51
+ end
52
+ end
53
+ end
54
+ end
@@ -6,7 +6,10 @@ module FormHelpers
6
6
  window.lastSubmittedForm = null;
7
7
 
8
8
  document.addEventListener('submit', (e) => {
9
- e.preventDefault();
9
+ if (!e.target.hasAttribute('data-turbo-frame')) {
10
+ e.preventDefault();
11
+ }
12
+
10
13
  window.lastSubmittedForm = e.target.id;
11
14
  });
12
15
  JS
@@ -138,6 +138,59 @@ RSpec.describe CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer do
138
138
  nonce: 'true'
139
139
  })
140
140
  end
141
+
142
+ context 'with lazy loading' do
143
+ subject(:html) { described_class.new(bundle, lazy: true).to_html }
144
+
145
+ it 'does not include preload tags' do
146
+ expect(html).not_to have_tag('link', with: { rel: 'preload' })
147
+ expect(html).not_to have_tag('link', with: { rel: 'modulepreload' })
148
+ end
149
+
150
+ it 'does not include stylesheet links' do
151
+ stylesheets.each do |url|
152
+ expect(html).not_to have_tag('link', with: { href: url, rel: 'stylesheet' })
153
+ end
154
+ end
155
+
156
+ it 'does not include window script tags' do
157
+ scripts.each do |script|
158
+ expect(html).not_to have_tag('script', with: { src: script.url }) if script.window?
159
+ end
160
+ end
161
+
162
+ it 'includes web component script' do
163
+ expect(html).to have_tag('script', with: { type: 'module' })
164
+ end
165
+ end
166
+
167
+ context 'with eager loading (lazy: false)' do
168
+ subject(:html) { described_class.new(bundle, lazy: false).to_html }
169
+
170
+ it 'includes preload tags' do
171
+ scripts.each do |script|
172
+ expect(html).to have_tag('link', with: {
173
+ href: script.url,
174
+ rel: script.esm? ? 'modulepreload' : 'preload'
175
+ })
176
+ end
177
+ end
178
+
179
+ it 'includes stylesheet links' do
180
+ stylesheets.each do |url|
181
+ expect(html).to have_tag('link', with: {
182
+ href: url,
183
+ rel: 'stylesheet'
184
+ })
185
+ end
186
+ end
187
+
188
+ it 'includes window script tags' do
189
+ scripts.each do |script|
190
+ expect(html).to have_tag('script', with: { src: script.url }) if script.window?
191
+ end
192
+ end
193
+ end
141
194
  end
142
195
 
143
196
  describe '.url_resource_preload_type' do
@@ -115,7 +115,8 @@ RSpec.describe CKEditor5::Rails::Assets::JSUrlImportMeta do
115
115
  expect(meta.to_h).to eq({
116
116
  url: url,
117
117
  import_name: 'module',
118
- import_as: 'alias'
118
+ import_as: 'alias',
119
+ translation: false
119
120
  })
120
121
  end
121
122
  end
@@ -35,10 +35,15 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
35
35
  end
36
36
 
37
37
  before do
38
- allow(CKEditor5::Rails::Engine).to receive(:find_preset).and_return(preset)
38
+ allow(CKEditor5::Rails::Engine).to receive(:find_preset!).and_return(preset)
39
39
  allow(CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer).to receive(:new).and_return(serializer)
40
40
  end
41
41
 
42
+ after do
43
+ RSpec::Mocks.space.proxy_for(CKEditor5::Rails::Engine).reset
44
+ RSpec::Mocks.space.proxy_for(CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer).reset
45
+ end
46
+
42
47
  describe '#ckeditor5_assets' do
43
48
  context 'with valid preset' do
44
49
  it 'creates base bundle' do
@@ -206,12 +211,13 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
206
211
 
207
212
  context 'destructure non-matching preset override' do
208
213
  before do
209
- allow(CKEditor5::Rails::Engine).to receive(:find_preset).and_return(nil)
214
+ RSpec::Mocks.space.proxy_for(CKEditor5::Rails::Engine).reset
210
215
  end
211
216
 
212
217
  it 'raises error' do
213
218
  expect { helper.ckeditor5_assets(preset: :invalid) }
214
- .to raise_error(ArgumentError, /forgot to define your invalid preset/)
219
+ .to raise_error(CKEditor5::Rails::PresetNotFoundError)
220
+ RSpec::Mocks.space.proxy_for(CKEditor5::Rails::Engine).reset
215
221
  end
216
222
  end
217
223
 
@@ -256,6 +262,92 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
256
262
  end
257
263
  end
258
264
 
265
+ describe '#ckeditor5_lazy_javascript_tags' do
266
+ let(:web_component_html) do
267
+ '<script type="module" src="web-component.js">web component code</script>'.html_safe
268
+ end
269
+
270
+ let(:import_map_html) { '<script type="importmap">{"imports":{}}</script>'.html_safe }
271
+
272
+ let(:web_component_bundle) do
273
+ instance_double(CKEditor5::Rails::Assets::WebComponentBundle, to_html: web_component_html)
274
+ end
275
+ let(:import_map_bundle) do
276
+ instance_double(CKEditor5::Rails::Assets::AssetsImportMap, to_html: import_map_html)
277
+ end
278
+ let(:preset_manager) { instance_double(CKEditor5::Rails::Presets::Manager) }
279
+ let(:test_preset1) { instance_double(CKEditor5::Rails::Presets::PresetBuilder) }
280
+ let(:test_preset2) { instance_double(CKEditor5::Rails::Presets::PresetBuilder) }
281
+
282
+ before do
283
+ allow(CKEditor5::Rails::Assets::WebComponentBundle).to receive(:instance).and_return(
284
+ web_component_bundle
285
+ )
286
+
287
+ allow(CKEditor5::Rails::Assets::AssetsImportMap).to receive(:new).and_return(
288
+ import_map_bundle
289
+ )
290
+
291
+ allow(CKEditor5::Rails::Engine).to receive(:presets).and_return(preset_manager)
292
+ allow(preset_manager).to receive(:to_h).and_return({
293
+ test1: test_preset1,
294
+ test2: test_preset2
295
+ })
296
+
297
+ allow(helper).to receive(:create_preset_bundle).with(test_preset1)
298
+ .and_return(CKEditor5::Rails::Assets::AssetsBundle.new(
299
+ scripts: ['test1.js']
300
+ ))
301
+
302
+ allow(helper).to receive(:create_preset_bundle).with(test_preset2)
303
+ .and_return(CKEditor5::Rails::Assets::AssetsBundle.new(
304
+ scripts: ['test2.js']
305
+ ))
306
+ end
307
+
308
+ context 'when importmap is available' do
309
+ before do
310
+ allow(helper).to receive(:importmap_available?).and_return(true)
311
+ allow(helper).to receive(:importmap_rendered?).and_return(false)
312
+ end
313
+
314
+ it 'stores bundle in context and returns web component script' do
315
+ result = helper.ckeditor5_lazy_javascript_tags
316
+
317
+ expect(result).to have_tag('script', with: {
318
+ type: 'module',
319
+ src: 'web-component.js'
320
+ })
321
+ expect(context[:bundle].scripts).to match_array(['test1.js', 'test2.js'])
322
+ end
323
+
324
+ it 'raises error when importmap is already rendered' do
325
+ allow(helper).to receive(:importmap_rendered?).and_return(true)
326
+
327
+ expect { helper.ckeditor5_lazy_javascript_tags }
328
+ .to raise_error(CKEditor5::Rails::Cdn::Helpers::ImportmapAlreadyRenderedError)
329
+ end
330
+ end
331
+
332
+ context 'when importmap is not available' do
333
+ before do
334
+ allow(helper).to receive(:importmap_available?).and_return(false)
335
+ end
336
+
337
+ it 'returns both importmap and web component scripts as one string' do
338
+ result = helper.ckeditor5_lazy_javascript_tags
339
+
340
+ expect(result).to have_tag('script', with: { type: 'importmap' },
341
+ text: '{"imports":{}}')
342
+
343
+ expect(result).to have_tag('script', with: {
344
+ type: 'module',
345
+ src: 'web-component.js'
346
+ })
347
+ end
348
+ end
349
+ end
350
+
259
351
  describe 'cdn helper methods' do
260
352
  it 'generates helper methods for third-party CDNs' do
261
353
  expect(helper).to respond_to(:ckeditor5_unpkg_assets)