ckeditor5 1.20.1 → 1.22.0

Sign up to get free protection for your applications and to get access to all the features.
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)