ckeditor5 1.15.8 → 1.15.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +6 -5
  3. data/README.md +3 -0
  4. data/lib/ckeditor5/rails/cdn/ckbox_bundle.rb +2 -2
  5. data/lib/ckeditor5/rails/cdn/helpers.rb +6 -0
  6. data/lib/ckeditor5/rails/context/helpers.rb +1 -1
  7. data/lib/ckeditor5/rails/editor/editable_height_normalizer.rb +50 -0
  8. data/lib/ckeditor5/rails/editor/helpers/config_helpers.rb +3 -3
  9. data/lib/ckeditor5/rails/editor/helpers/editor_helpers.rb +5 -4
  10. data/lib/ckeditor5/rails/editor/props.rb +3 -20
  11. data/lib/ckeditor5/rails/plugins/simple_upload_adapter.rb +1 -1
  12. data/lib/ckeditor5/rails/presets/preset_builder.rb +2 -3
  13. data/lib/ckeditor5/rails/version.rb +1 -1
  14. data/lib/ckeditor5/rails/version_detector.rb +6 -0
  15. data/spec/lib/ckeditor5/rails/assets/asset_bundle_hml_serializer_spec.rb +104 -0
  16. data/spec/lib/ckeditor5/rails/assets/assets_bundle_spec.rb +191 -0
  17. data/spec/lib/ckeditor5/rails/cdn/ckbox_bundle_spec.rb +69 -0
  18. data/spec/lib/ckeditor5/rails/cdn/ckeditor_bundle_spec.rb +72 -0
  19. data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +217 -0
  20. data/spec/lib/ckeditor5/rails/context/helpers_spec.rb +67 -0
  21. data/spec/lib/ckeditor5/rails/context/props_spec.rb +70 -0
  22. data/spec/lib/ckeditor5/rails/editor/editable_height_normalizer_spec.rb +50 -0
  23. data/spec/lib/ckeditor5/rails/editor/helpers/config_helpers_spec.rb +52 -0
  24. data/spec/lib/ckeditor5/rails/editor/helpers/editor_helpers_spec.rb +192 -0
  25. data/spec/lib/ckeditor5/rails/editor/props_inline_plugin_spec.rb +43 -0
  26. data/spec/lib/ckeditor5/rails/editor/props_plugin_spec.rb +66 -0
  27. data/spec/lib/ckeditor5/rails/editor/props_spec.rb +104 -0
  28. data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +47 -0
  29. data/spec/lib/ckeditor5/rails/presets/manager_spec.rb +100 -0
  30. data/spec/lib/ckeditor5/rails/presets/plugins_builder_spec.rb +98 -0
  31. data/spec/lib/ckeditor5/rails/presets/preset_builder_spec.rb +337 -0
  32. data/spec/lib/ckeditor5/rails/presets/toolbar_builder_spec.rb +70 -0
  33. data/spec/lib/ckeditor5/rails/semver_spec.rb +58 -0
  34. data/spec/lib/ckeditor5/rails/version_detector_spec.rb +131 -0
  35. data/spec/lib/ckeditor5/rails/version_spec.rb +25 -0
  36. data/spec/spec_helper.rb +8 -2
  37. data/spec/support/test_models.rb +6 -0
  38. metadata +44 -1
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'action_view'
5
+
6
+ RSpec.describe CKEditor5::Rails::Editor::Helpers::Editor do
7
+ let(:test_class) do
8
+ Class.new do
9
+ include ActionView::Helpers::TagHelper
10
+ include CKEditor5::Rails::Editor::Helpers::Editor
11
+ end
12
+ end
13
+
14
+ let(:helper) { test_class.new }
15
+ let(:preset) { instance_double(CKEditor5::Rails::Presets::PresetBuilder) }
16
+ let(:context) { { preset: :default, cdn: :jsdelivr } }
17
+
18
+ before do
19
+ helper.instance_variable_set(:@__ckeditor_context, context)
20
+ allow(preset).to receive(:type).and_return(:classic)
21
+ allow(preset).to receive(:config).and_return({})
22
+ allow(preset).to receive(:automatic_upgrades?).and_return(false)
23
+ end
24
+
25
+ describe '#ckeditor5_editor' do
26
+ before do
27
+ allow(helper).to receive(:find_preset).and_return(preset)
28
+ end
29
+
30
+ it 'raises error when context is not defined' do
31
+ helper.remove_instance_variable(:@__ckeditor_context)
32
+ expect { helper.ckeditor5_editor }.to raise_error(
33
+ described_class::EditorContextError,
34
+ /CKEditor installation context is not defined/
35
+ )
36
+ end
37
+
38
+ it 'raises error when preset is not found' do
39
+ allow(helper).to receive(:find_preset).and_raise(described_class::PresetNotFoundError)
40
+ expect do
41
+ helper.ckeditor5_editor(preset: :unknown)
42
+ end.to raise_error(described_class::PresetNotFoundError)
43
+ end
44
+
45
+ it 'merges extra configuration with preset config' do
46
+ extra_config = { toolbar: { items: ['bold'] } }
47
+ expect(helper).to receive(:build_editor_config)
48
+ .with(preset, nil, extra_config, nil)
49
+ .and_return(extra_config)
50
+
51
+ helper.ckeditor5_editor(extra_config: extra_config)
52
+ end
53
+
54
+ it 'sets initial data in config when provided' do
55
+ expect(helper).to receive(:build_editor_config)
56
+ .with(preset, nil, {}, 'initial content')
57
+ .and_call_original
58
+
59
+ helper.ckeditor5_editor(initial_data: 'initial content')
60
+ end
61
+
62
+ it 'cannot have both initial_data and block content' do
63
+ expect do
64
+ helper.ckeditor5_editor(initial_data: 'content') { 'block content' }
65
+ end.to raise_error(ArgumentError, /Cannot pass initial data and block/)
66
+ end
67
+
68
+ context 'when automatic upgrades are enabled' do
69
+ before do
70
+ allow(preset).to receive(:automatic_upgrades?).and_return(true)
71
+ end
72
+
73
+ it 'updates version when detector finds newer safe version' do
74
+ extra_config = { version: '35.1.0' }
75
+ allow(CKEditor5::Rails::VersionDetector).to receive(:latest_safe_version)
76
+ .with('35.1.0')
77
+ .and_return('35.3.0')
78
+
79
+ result = helper.ckeditor5_editor(extra_config: extra_config)
80
+ expect(result).to include('35.3.0')
81
+ end
82
+
83
+ it 'keeps original version when no safe upgrade is available' do
84
+ extra_config = { version: '35.1.0' }
85
+ allow(CKEditor5::Rails::VersionDetector).to receive(:latest_safe_version)
86
+ .with('35.1.0')
87
+ .and_return(nil)
88
+
89
+ result = helper.ckeditor5_editor(extra_config: extra_config)
90
+ expect(result).to include('35.1.0')
91
+ end
92
+
93
+ it 'skips version detection when version is not specified' do
94
+ expect(CKEditor5::Rails::VersionDetector).not_to receive(:latest_safe_version)
95
+ helper.ckeditor5_editor
96
+ end
97
+ end
98
+
99
+ context 'when automatic upgrades are disabled' do
100
+ before do
101
+ allow(preset).to receive(:automatic_upgrades?).and_return(false)
102
+ end
103
+
104
+ it 'does not modify version even when newer version is available' do
105
+ extra_config = { version: '35.1.0' }
106
+ expect(CKEditor5::Rails::VersionDetector).not_to receive(:latest_safe_version)
107
+
108
+ result = helper.ckeditor5_editor(extra_config: extra_config)
109
+ expect(result).to include('35.1.0')
110
+ end
111
+ end
112
+
113
+ context 'when using preset lookup' do
114
+ before do
115
+ RSpec::Mocks.space.proxy_for(helper).remove_stub(:find_preset)
116
+ end
117
+
118
+ it 'uses default preset when none specified' do
119
+ expect(CKEditor5::Rails::Engine).to receive(:find_preset)
120
+ .with(:default)
121
+ .and_return(preset)
122
+
123
+ helper.ckeditor5_editor
124
+ end
125
+
126
+ it 'uses preset from context when available' do
127
+ helper.instance_variable_set(:@__ckeditor_context, { preset: :custom })
128
+ expect(CKEditor5::Rails::Engine).to receive(:find_preset)
129
+ .with(:custom)
130
+ .and_return(preset)
131
+
132
+ helper.ckeditor5_editor
133
+ end
134
+
135
+ it 'prefers explicitly passed preset over context preset' do
136
+ helper.instance_variable_set(:@__ckeditor_context, { preset: :from_context })
137
+ expect(CKEditor5::Rails::Engine).to receive(:find_preset)
138
+ .with(:explicit)
139
+ .and_return(preset)
140
+
141
+ helper.ckeditor5_editor(preset: :explicit)
142
+ end
143
+
144
+ it 'raises error when preset cannot be found' do
145
+ allow(CKEditor5::Rails::Engine).to receive(:find_preset)
146
+ .with(:unknown)
147
+ .and_return(nil)
148
+
149
+ expect { helper.ckeditor5_editor(preset: :unknown) }.to raise_error(
150
+ described_class::PresetNotFoundError,
151
+ 'Preset unknown is not defined.'
152
+ )
153
+ end
154
+ end
155
+ end
156
+
157
+ describe '#ckeditor5_editable' do
158
+ it 'creates editable component with name' do
159
+ expect(helper.ckeditor5_editable('content')).to have_tag(
160
+ 'ckeditor-editable-component',
161
+ with: { name: 'content' }
162
+ )
163
+ end
164
+ end
165
+
166
+ describe '#ckeditor5_ui_part' do
167
+ it 'creates ui part component with name' do
168
+ expect(helper.ckeditor5_ui_part('toolbar')).to have_tag(
169
+ 'ckeditor-ui-part-component',
170
+ with: { name: 'toolbar' }
171
+ )
172
+ end
173
+ end
174
+
175
+ describe '#ckeditor5_toolbar' do
176
+ it 'creates toolbar ui part' do
177
+ expect(helper.ckeditor5_toolbar).to have_tag(
178
+ 'ckeditor-ui-part-component',
179
+ with: { name: 'toolbar' }
180
+ )
181
+ end
182
+ end
183
+
184
+ describe '#ckeditor5_menubar' do
185
+ it 'creates menubar ui part' do
186
+ expect(helper.ckeditor5_menubar).to have_tag(
187
+ 'ckeditor-ui-part-component',
188
+ with: { name: 'menuBarView' }
189
+ )
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Editor::PropsInlinePlugin do
6
+ let(:valid_code) do
7
+ <<~JAVASCRIPT
8
+ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
9
+ export default class CustomPlugin extends Plugin {
10
+ init() {
11
+ console.log('Custom plugin initialized');
12
+ }
13
+ }
14
+ JAVASCRIPT
15
+ end
16
+
17
+ describe '#initialize' do
18
+ it 'accepts valid plugin code' do
19
+ expect { described_class.new(:CustomPlugin, valid_code) }.not_to raise_error
20
+ end
21
+
22
+ it 'raises error when code is not a string' do
23
+ expect { described_class.new(:CustomPlugin, nil) }
24
+ .to raise_error(ArgumentError, 'Code must be a String')
25
+ end
26
+
27
+ it 'raises error when code lacks export default' do
28
+ expect { described_class.new(:CustomPlugin, 'class CustomPlugin {}') }
29
+ .to raise_error(ArgumentError, /must include `export default`/)
30
+ end
31
+ end
32
+
33
+ describe '#to_h' do
34
+ it 'returns correct hash representation' do
35
+ plugin = described_class.new(:CustomPlugin, valid_code)
36
+ expect(plugin.to_h).to eq({
37
+ type: :inline,
38
+ name: :CustomPlugin,
39
+ code: valid_code
40
+ })
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Editor::PropsPlugin do
6
+ describe '.normalize' do
7
+ it 'converts string to plugin instance' do
8
+ plugin = described_class.normalize('Bold')
9
+ expect(plugin).to be_a(described_class)
10
+ expect(plugin.name).to eq('Bold')
11
+ end
12
+
13
+ it 'converts symbol to plugin instance' do
14
+ plugin = described_class.normalize(:Bold)
15
+ expect(plugin).to be_a(described_class)
16
+ expect(plugin.name).to eq(:Bold)
17
+ end
18
+
19
+ it 'returns existing plugin instances unchanged' do
20
+ original = described_class.new(:Bold)
21
+ plugin = described_class.normalize(original)
22
+ expect(plugin).to be(original)
23
+ end
24
+
25
+ it 'returns inline plugin instances unchanged' do
26
+ inline = CKEditor5::Rails::Editor::PropsInlinePlugin.new(:Custom, 'export default class {}')
27
+ plugin = described_class.normalize(inline)
28
+ expect(plugin).to be(inline)
29
+ end
30
+
31
+ it 'raises error for invalid input' do
32
+ expect { described_class.normalize({}) }.to raise_error(ArgumentError)
33
+ end
34
+ end
35
+
36
+ describe '#to_h' do
37
+ it 'generates hash for standard plugin' do
38
+ plugin = described_class.new(:Bold)
39
+ expect(plugin.to_h).to include(
40
+ type: :external,
41
+ import_name: 'ckeditor5',
42
+ import_as: :Bold
43
+ )
44
+ end
45
+
46
+ it 'generates hash for premium plugin' do
47
+ plugin = described_class.new(:Bold, premium: true)
48
+ expect(plugin.to_h).to include(
49
+ type: :external,
50
+ import_name: 'ckeditor5-premium-features',
51
+ import_as: :Bold
52
+ )
53
+ end
54
+
55
+ it 'handles custom import metadata' do
56
+ plugin = described_class.new(:Custom,
57
+ import_name: 'custom-module',
58
+ window_name: 'CustomPlugin')
59
+ expect(plugin.to_h).to include(
60
+ type: :external,
61
+ import_name: 'custom-module',
62
+ window_name: 'CustomPlugin'
63
+ )
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Editor::Props do
6
+ let(:controller_context) do
7
+ {
8
+ bundle: double('Bundle', translations_scripts: [{ path: 'translations/en.js' }]),
9
+ license_key: nil
10
+ }
11
+ end
12
+ let(:type) { :classic }
13
+ let(:config) { { plugins: [], toolbar: { items: [] } } }
14
+
15
+ describe '#initialize' do
16
+ it 'accepts valid editor type' do
17
+ expect { described_class.new(controller_context, :classic, {}) }.not_to raise_error
18
+ end
19
+
20
+ it 'raises error for invalid editor type' do
21
+ expect { described_class.new(controller_context, :invalid, {}) }
22
+ .to raise_error(ArgumentError, 'Invalid editor type: invalid')
23
+ end
24
+ end
25
+
26
+ describe '#to_attributes' do
27
+ subject(:props) { described_class.new(controller_context, type, config) }
28
+
29
+ it 'includes required attributes' do
30
+ attributes = props.to_attributes
31
+ expect(attributes).to include(
32
+ type: 'ClassicEditor',
33
+ translations: String,
34
+ plugins: String,
35
+ config: String,
36
+ watchdog: true
37
+ )
38
+ end
39
+
40
+ context 'with editable height' do
41
+ subject(:props) { described_class.new(controller_context, type, config, editable_height: '500px') }
42
+
43
+ it 'includes editable-height attribute' do
44
+ expect(props.to_attributes['editable-height']).to eq('500px')
45
+ end
46
+ end
47
+
48
+ context 'with license key' do
49
+ let(:controller_context) do
50
+ { bundle: double('Bundle', translations_scripts: []), license_key: 'ABC123' }
51
+ end
52
+
53
+ it 'includes license key in config' do
54
+ config_json = props.to_attributes[:config]
55
+ expect(config_json).to include('licenseKey')
56
+ expect(JSON.parse(config_json)['licenseKey']).to eq('ABC123')
57
+ end
58
+ end
59
+ end
60
+
61
+ describe '.valid_editor_type?' do
62
+ it 'returns true for valid types' do
63
+ %i[classic inline balloon decoupled multiroot].each do |type|
64
+ expect(described_class.valid_editor_type?(type)).to be true
65
+ end
66
+ end
67
+
68
+ it 'returns false for invalid types' do
69
+ expect(described_class.valid_editor_type?(:invalid)).to be false
70
+ end
71
+ end
72
+
73
+ describe 'editable height validation' do
74
+ context 'with non-classic editor' do
75
+ let(:type) { :inline }
76
+
77
+ it 'raises error when editable height is set' do
78
+ expect do
79
+ described_class.new(controller_context, type, config, editable_height: '500px')
80
+ end.to raise_error(CKEditor5::Rails::Editor::InvalidEditableHeightError)
81
+ end
82
+ end
83
+
84
+ context 'with classic editor' do
85
+ let(:type) { :classic }
86
+
87
+ it 'accepts integer values' do
88
+ props = described_class.new(controller_context, type, config, editable_height: 500)
89
+ expect(props.to_attributes['editable-height']).to eq('500px')
90
+ end
91
+
92
+ it 'accepts pixel string values' do
93
+ props = described_class.new(controller_context, type, config, editable_height: '500px')
94
+ expect(props.to_attributes['editable-height']).to eq('500px')
95
+ end
96
+
97
+ it 'raises error for invalid values' do
98
+ expect do
99
+ described_class.new(controller_context, type, config, editable_height: '500em')
100
+ end.to raise_error(CKEditor5::Rails::Editor::InvalidEditableHeightError)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Hooks::Form do
6
+ describe CKEditor5::Rails::Hooks::Form::EditorInputBuilder do
7
+ let(:post) { Post.new(content: 'Initial content') }
8
+ let(:builder) { described_class.new(:post, post, template) }
9
+ let(:template) do
10
+ ActionView::Base.new(ActionView::LookupContext.new([]), {}, nil)
11
+ end
12
+
13
+ before do
14
+ template.ckeditor5_assets(version: '34.1.0')
15
+ end
16
+
17
+ describe '#build_editor' do
18
+ subject(:rendered_editor) { builder.build_editor(:content) }
19
+
20
+ it 'renders ckeditor element' do
21
+ attrs = {
22
+ name: 'post[content]',
23
+ id: 'post_content',
24
+ type: 'ClassicEditor',
25
+ translations: '[{"import_name":"ckeditor5/translations/en.js"}]',
26
+ watchdog: 'true'
27
+ }
28
+
29
+ expect(rendered_editor).to have_tag('ckeditor-component', with: attrs)
30
+ end
31
+
32
+ context 'with custom attributes' do
33
+ subject(:rendered_editor) do
34
+ builder.build_editor(:content, class: 'custom-class', id: 'custom-id', name: 'custom-name')
35
+ end
36
+
37
+ it 'respects custom HTML attributes' do
38
+ expect(rendered_editor).to have_tag('ckeditor-component', with: {
39
+ class: 'custom-class',
40
+ id: 'custom-id',
41
+ name: 'custom-name'
42
+ })
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Presets::Manager do
6
+ subject(:manager) { described_class.new }
7
+
8
+ describe '#initialize' do
9
+ it 'creates empty presets hash' do
10
+ expect(manager.presets).to be_a(Hash)
11
+ end
12
+
13
+ it 'defines default preset' do
14
+ expect(manager.default).to be_a(CKEditor5::Rails::Presets::PresetBuilder)
15
+ end
16
+ end
17
+
18
+ describe '#define' do
19
+ context 'with inheritance' do
20
+ it 'creates new preset based on default' do
21
+ manager.define(:custom) do
22
+ automatic_upgrades enabled: false
23
+ version '36.0.0'
24
+ end
25
+
26
+ expect(manager[:custom].version).to eq('36.0.0')
27
+ expect(manager[:custom].type).to eq(manager.default.type)
28
+ end
29
+ end
30
+
31
+ context 'without inheritance' do
32
+ it 'creates completely new preset' do
33
+ manager.define(:custom, inherit: false) do
34
+ automatic_upgrades enabled: false
35
+ version '36.0.0'
36
+ end
37
+
38
+ expect(manager[:custom].version).to eq('36.0.0')
39
+ expect(manager[:custom].config).to eq({ plugins: [], toolbar: [] })
40
+ end
41
+ end
42
+ end
43
+
44
+ describe '#override/#extend' do
45
+ before do
46
+ manager.define(:custom) do
47
+ automatic_upgrades enabled: false
48
+ version '35.0.0'
49
+ toolbar :bold
50
+ end
51
+ end
52
+
53
+ it 'modifies existing preset' do
54
+ manager.override(:custom) do
55
+ automatic_upgrades enabled: false
56
+ version '36.0.0'
57
+ toolbar :italic
58
+ end
59
+
60
+ expect(manager[:custom].version).to eq('36.0.0')
61
+ expect(manager[:custom].config[:toolbar][:items]).to eq([:italic])
62
+ end
63
+
64
+ it 'allows using extend as alias for override' do
65
+ manager.extend(:custom) do
66
+ automatic_upgrades enabled: false
67
+ version '36.0.0'
68
+ end
69
+
70
+ expect(manager[:custom].version).to eq('36.0.0')
71
+ end
72
+ end
73
+
74
+ describe '#[]' do
75
+ it 'returns preset by name' do
76
+ manager.define(:custom) do
77
+ automatic_upgrades enabled: false
78
+ version '36.0.0'
79
+ end
80
+
81
+ expect(manager[:custom]).to be_a(CKEditor5::Rails::Presets::PresetBuilder)
82
+ expect(manager[:custom].version).to eq('36.0.0')
83
+ end
84
+
85
+ it 'returns nil for non-existent preset' do
86
+ expect(manager[:non_existent]).to be_nil
87
+ end
88
+ end
89
+
90
+ describe '#default' do
91
+ it 'has default configuration' do
92
+ expect(manager.default.version).to eq(CKEditor5::Rails::DEFAULT_CKEDITOR_VERSION)
93
+ expect(manager.default.type).to eq(:classic)
94
+ expect(manager.default.automatic_upgrades?).to be true
95
+ expect(manager.default.menubar?).to be true
96
+ expect(manager.default.config[:plugins]).not_to be_empty
97
+ expect(manager.default.config[:toolbar][:items]).not_to be_empty
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CKEditor5::Rails::Presets::PluginsBuilder do
6
+ let(:plugins) { [] }
7
+ let(:builder) { described_class.new(plugins) }
8
+
9
+ describe '.create_plugin' do
10
+ context 'when name is a string' do
11
+ it 'creates a new PropsPlugin' do
12
+ plugin = described_class.create_plugin('Test')
13
+ expect(plugin).to be_a(CKEditor5::Rails::Editor::PropsPlugin)
14
+ expect(plugin.name).to eq('Test')
15
+ end
16
+ end
17
+
18
+ context 'when name is already a plugin instance' do
19
+ let(:existing_plugin) { CKEditor5::Rails::Editor::PropsPlugin.new('Test') }
20
+
21
+ it 'returns the plugin instance unchanged' do
22
+ plugin = described_class.create_plugin(existing_plugin)
23
+ expect(plugin).to eq(existing_plugin)
24
+ end
25
+ end
26
+ end
27
+
28
+ describe '#remove' do
29
+ before do
30
+ plugins.push(
31
+ CKEditor5::Rails::Editor::PropsPlugin.new('Plugin1'),
32
+ CKEditor5::Rails::Editor::PropsPlugin.new('Plugin2'),
33
+ CKEditor5::Rails::Editor::PropsPlugin.new('Plugin3')
34
+ )
35
+ end
36
+
37
+ it 'removes specified plugins' do
38
+ builder.remove('Plugin1', 'Plugin3')
39
+ expect(plugins.map(&:name)).to eq(['Plugin2'])
40
+ end
41
+ end
42
+
43
+ describe '#prepend' do
44
+ let(:existing_plugin) { CKEditor5::Rails::Editor::PropsPlugin.new('ExistingPlugin') }
45
+
46
+ before do
47
+ plugins.push(existing_plugin)
48
+ end
49
+
50
+ context 'without before option' do
51
+ it 'adds plugins at the beginning' do
52
+ builder.prepend('NewPlugin1', 'NewPlugin2')
53
+ expect(plugins.map(&:name)).to eq(%w[NewPlugin1 NewPlugin2 ExistingPlugin])
54
+ end
55
+ end
56
+
57
+ context 'with before option' do
58
+ it 'adds plugins before specified plugin' do
59
+ builder.prepend('NewPlugin', before: 'ExistingPlugin')
60
+ expect(plugins.map(&:name)).to eq(%w[NewPlugin ExistingPlugin])
61
+ end
62
+
63
+ it 'raises error when target plugin not found' do
64
+ expect do
65
+ builder.prepend('NewPlugin', before: 'NonExistent')
66
+ end.to raise_error(ArgumentError, "Plugin 'NonExistent' not found")
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#append' do
72
+ let(:existing_plugin) { CKEditor5::Rails::Editor::PropsPlugin.new('ExistingPlugin') }
73
+
74
+ before do
75
+ plugins.push(existing_plugin)
76
+ end
77
+
78
+ context 'without after option' do
79
+ it 'adds plugins at the end' do
80
+ builder.append('NewPlugin1', 'NewPlugin2')
81
+ expect(plugins.map(&:name)).to eq(%w[ExistingPlugin NewPlugin1 NewPlugin2])
82
+ end
83
+ end
84
+
85
+ context 'with after option' do
86
+ it 'adds plugins after specified plugin' do
87
+ builder.append('NewPlugin', after: 'ExistingPlugin')
88
+ expect(plugins.map(&:name)).to eq(%w[ExistingPlugin NewPlugin])
89
+ end
90
+
91
+ it 'raises error when target plugin not found' do
92
+ expect do
93
+ builder.append('NewPlugin', after: 'NonExistent')
94
+ end.to raise_error(ArgumentError, "Plugin 'NonExistent' not found")
95
+ end
96
+ end
97
+ end
98
+ end