ckeditor5 1.15.8 → 1.15.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) 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/engine.rb +3 -2
  12. data/lib/ckeditor5/rails/plugins/simple_upload_adapter.rb +1 -1
  13. data/lib/ckeditor5/rails/presets/plugins_builder.rb +9 -9
  14. data/lib/ckeditor5/rails/presets/preset_builder.rb +6 -9
  15. data/lib/ckeditor5/rails/version.rb +1 -1
  16. data/lib/ckeditor5/rails/version_detector.rb +6 -0
  17. data/spec/lib/ckeditor5/rails/assets/asset_bundle_hml_serializer_spec.rb +111 -0
  18. data/spec/lib/ckeditor5/rails/assets/assets_bundle_spec.rb +191 -0
  19. data/spec/lib/ckeditor5/rails/cdn/ckbox_bundle_spec.rb +69 -0
  20. data/spec/lib/ckeditor5/rails/cdn/ckeditor_bundle_spec.rb +72 -0
  21. data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +235 -0
  22. data/spec/lib/ckeditor5/rails/context/helpers_spec.rb +67 -0
  23. data/spec/lib/ckeditor5/rails/context/props_spec.rb +70 -0
  24. data/spec/lib/ckeditor5/rails/editor/editable_height_normalizer_spec.rb +50 -0
  25. data/spec/lib/ckeditor5/rails/editor/helpers/config_helpers_spec.rb +52 -0
  26. data/spec/lib/ckeditor5/rails/editor/helpers/editor_helpers_spec.rb +192 -0
  27. data/spec/lib/ckeditor5/rails/editor/props_inline_plugin_spec.rb +43 -0
  28. data/spec/lib/ckeditor5/rails/editor/props_plugin_spec.rb +66 -0
  29. data/spec/lib/ckeditor5/rails/editor/props_spec.rb +104 -0
  30. data/spec/lib/ckeditor5/rails/engine_spec.rb +88 -0
  31. data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +203 -0
  32. data/spec/lib/ckeditor5/rails/hooks/simple_form_spec.rb +100 -0
  33. data/spec/lib/ckeditor5/rails/presets/manager_spec.rb +100 -0
  34. data/spec/lib/ckeditor5/rails/presets/plugins_builder_spec.rb +98 -0
  35. data/spec/lib/ckeditor5/rails/presets/preset_builder_spec.rb +347 -0
  36. data/spec/lib/ckeditor5/rails/presets/toolbar_builder_spec.rb +70 -0
  37. data/spec/lib/ckeditor5/rails/semver_spec.rb +58 -0
  38. data/spec/lib/ckeditor5/rails/version_detector_spec.rb +131 -0
  39. data/spec/lib/ckeditor5/rails/version_spec.rb +25 -0
  40. data/spec/spec_helper.rb +8 -2
  41. data/spec/support/test_models.rb +6 -0
  42. metadata +49 -2
@@ -0,0 +1,203 @@
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
+
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
201
+ end
202
+ end
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
@@ -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(:items) { [] }
7
+ let(:builder) { described_class.new(items) }
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
+ items.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(items.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
+ items.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(items.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(items.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
+ items.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(items.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(items.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