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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/lib/ckeditor5/rails/assets/webcomponents/components/context.mjs +23 -0
- data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +24 -0
- data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +33 -10
- data/lib/ckeditor5/rails/concerns/checksum.rb +15 -0
- data/lib/ckeditor5/rails/context/props.rb +17 -2
- data/lib/ckeditor5/rails/editor/props.rb +16 -3
- data/lib/ckeditor5/rails/editor/props_base_plugin.rb +19 -0
- data/lib/ckeditor5/rails/editor/props_inline_plugin.rb +6 -3
- data/lib/ckeditor5/rails/editor/props_plugin.rb +6 -4
- data/lib/ckeditor5/rails/engine.rb +3 -2
- data/lib/ckeditor5/rails/presets/plugins_builder.rb +9 -9
- data/lib/ckeditor5/rails/presets/preset_builder.rb +4 -6
- data/lib/ckeditor5/rails/version.rb +1 -1
- data/lib/ckeditor5/rails.rb +1 -0
- data/spec/e2e/features/editor_types_spec.rb +178 -0
- data/spec/e2e/features/form_integration_spec.rb +60 -0
- data/spec/e2e/spec_helper.rb +43 -0
- data/spec/e2e/support/eventually.rb +14 -0
- data/spec/e2e/support/form_helpers.rb +37 -0
- data/spec/lib/ckeditor5/rails/assets/asset_bundle_hml_serializer_spec.rb +7 -0
- data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +19 -1
- data/spec/lib/ckeditor5/rails/concerns/checksum_spec.rb +50 -0
- data/spec/lib/ckeditor5/rails/context/props_spec.rb +7 -1
- data/spec/lib/ckeditor5/rails/editor/props_base_plugin_spec.rb +27 -0
- data/spec/lib/ckeditor5/rails/editor/props_spec.rb +2 -0
- data/spec/lib/ckeditor5/rails/engine_spec.rb +88 -0
- data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +156 -0
- data/spec/lib/ckeditor5/rails/hooks/simple_form_spec.rb +100 -0
- data/spec/lib/ckeditor5/rails/presets/plugins_builder_spec.rb +10 -10
- data/spec/lib/ckeditor5/rails/presets/preset_builder_spec.rb +10 -0
- data/spec/spec_helper.rb +2 -2
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e91fe2d11179c703901756efa978fcedcd3030d0381c4e550ab8c9c15ef73a19
|
4
|
+
data.tar.gz: 4119948cdae51969b3d1e77801f20faecd81cfbf2755388fc444421e7da8bdb9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4f236d5bc883344daa8fa9bc0b7d55e3898360dd8fabfd48265bbd1eedf3c654e8ba6baf5b0a1ba5ffa215c6b2c14a60c02f7182b81885a875b0a4902f67746
|
7
|
+
data.tar.gz: 593de9199583c1f22a38b9858967eca63b9c98eef8b7a6a1e5a06ccfa1df759424dab96ed88c1719614744b26c8a32920722156875ba748e66c80272eeb0fd78
|
data/Gemfile
CHANGED
@@ -12,7 +12,12 @@ class CKEditorContextComponent extends HTMLElement {
|
|
12
12
|
/** @type {Set<CKEditorComponent>} */
|
13
13
|
#connectedEditors = new Set();
|
14
14
|
|
15
|
+
/** @type {String} Attributes checksum hash */
|
16
|
+
#integrity = '';
|
17
|
+
|
15
18
|
async connectedCallback() {
|
19
|
+
this.#integrity = this.getAttribute('integrity');
|
20
|
+
|
16
21
|
try {
|
17
22
|
execIfDOMReady(() => this.#initializeContext());
|
18
23
|
} catch (error) {
|
@@ -52,6 +57,22 @@ class CKEditorContextComponent extends HTMLElement {
|
|
52
57
|
this.#connectedEditors.delete(editor);
|
53
58
|
}
|
54
59
|
|
60
|
+
/**
|
61
|
+
* Validates editor configuration integrity hash to prevent attacks.
|
62
|
+
*/
|
63
|
+
async #validateIntegrity() {
|
64
|
+
const integrity = await calculateChecksum({
|
65
|
+
plugins: this.getAttribute('plugins'),
|
66
|
+
});
|
67
|
+
|
68
|
+
if (integrity !== this.#integrity) {
|
69
|
+
throw new Error(
|
70
|
+
'Configuration integrity check failed. It means that #integrity attributes mismatch from attributes passed to webcomponent. ' +
|
71
|
+
'This could be a security issue. Please check if you are passing correct attributes to the webcomponent.'
|
72
|
+
);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
55
76
|
/**
|
56
77
|
* Initialize CKEditor context with shared configuration
|
57
78
|
*
|
@@ -66,6 +87,8 @@ class CKEditorContextComponent extends HTMLElement {
|
|
66
87
|
this.instance = null;
|
67
88
|
}
|
68
89
|
|
90
|
+
await this.#validateIntegrity();
|
91
|
+
|
69
92
|
const { Context, ContextWatchdog } = await import('ckeditor5');
|
70
93
|
const plugins = await this.#getPlugins();
|
71
94
|
const config = this.#getConfig();
|
@@ -40,6 +40,9 @@ class CKEditorComponent extends HTMLElement {
|
|
40
40
|
/** @type {String} ID of editor within context */
|
41
41
|
#contextEditorId = null;
|
42
42
|
|
43
|
+
/** @type {String} Attributes checksum hash */
|
44
|
+
#integrity = '';
|
45
|
+
|
43
46
|
/** @type {(event: CustomEvent) => void} Event handler for editor change */
|
44
47
|
get oneditorchange() {
|
45
48
|
return this.#getEventHandler('editorchange');
|
@@ -106,9 +109,11 @@ class CKEditorComponent extends HTMLElement {
|
|
106
109
|
/**
|
107
110
|
* Lifecycle callback when element is connected to DOM
|
108
111
|
* Initializes the editor when DOM is ready
|
112
|
+
*
|
109
113
|
* @protected
|
110
114
|
*/
|
111
115
|
connectedCallback() {
|
116
|
+
this.#integrity = this.getAttribute('integrity');
|
112
117
|
this.#context = this.closest('ckeditor-context-component');
|
113
118
|
this.#initialHTML = this.innerHTML;
|
114
119
|
|
@@ -228,6 +233,23 @@ class CKEditorComponent extends HTMLElement {
|
|
228
233
|
return resolveElementReferences(config);
|
229
234
|
}
|
230
235
|
|
236
|
+
/**
|
237
|
+
* Validates editor configuration integrity hash to prevent attacks.
|
238
|
+
*/
|
239
|
+
async #validateIntegrity() {
|
240
|
+
const integrity = await calculateChecksum({
|
241
|
+
translations: this.getAttribute('translations'),
|
242
|
+
plugins: this.getAttribute('plugins'),
|
243
|
+
});
|
244
|
+
|
245
|
+
if (integrity !== this.#integrity) {
|
246
|
+
throw new Error(
|
247
|
+
'Configuration integrity check failed. It means that #integrity attributes mismatch from attributes passed to webcomponent. ' +
|
248
|
+
'This could be a security issue. Please check if you are passing correct attributes to the webcomponent.'
|
249
|
+
);
|
250
|
+
}
|
251
|
+
}
|
252
|
+
|
231
253
|
/**
|
232
254
|
* Creates a new CKEditor instance
|
233
255
|
*
|
@@ -237,6 +259,8 @@ class CKEditorComponent extends HTMLElement {
|
|
237
259
|
* @throws {Error} When initialization fails
|
238
260
|
*/
|
239
261
|
async #initializeEditor(editablesOrContent) {
|
262
|
+
await this.#validateIntegrity();
|
263
|
+
|
240
264
|
const Editor = await this.#getEditorConstructor();
|
241
265
|
const [plugins, translations] = await Promise.all([
|
242
266
|
this.#getPlugins(),
|
@@ -66,16 +66,18 @@ function loadAsyncImports(imports = []) {
|
|
66
66
|
return imported;
|
67
67
|
};
|
68
68
|
|
69
|
-
return Promise.all(
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
69
|
+
return Promise.all(
|
70
|
+
imports.map(async (item) => {
|
71
|
+
switch(item.type) {
|
72
|
+
case 'inline':
|
73
|
+
return loadInlinePlugin(item);
|
74
|
+
|
75
|
+
case 'external':
|
76
|
+
default:
|
77
|
+
return loadExternalPlugin(item);
|
78
|
+
}
|
79
|
+
})
|
80
|
+
);
|
79
81
|
}
|
80
82
|
|
81
83
|
/**
|
@@ -153,3 +155,24 @@ function resolveElementReferences(obj) {
|
|
153
155
|
function uid() {
|
154
156
|
return Math.random().toString(36).substring(2);
|
155
157
|
}
|
158
|
+
|
159
|
+
/**
|
160
|
+
* Calculates checksum for an object.
|
161
|
+
*/
|
162
|
+
async function calculateChecksum(obj) {
|
163
|
+
const objCopy = { ...obj, checksum: undefined };
|
164
|
+
|
165
|
+
return sha256(JSON.stringify(objCopy));
|
166
|
+
}
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Calculates SHA-256 hash for a string
|
170
|
+
*/
|
171
|
+
async function sha256(str) {
|
172
|
+
const buffer = new TextEncoder().encode(str);
|
173
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
174
|
+
|
175
|
+
return Array.from(new Uint8Array(hashBuffer))
|
176
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
177
|
+
.join('');
|
178
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module CKEditor5::Rails::Concerns
|
7
|
+
module Checksum
|
8
|
+
private
|
9
|
+
|
10
|
+
def calculate_object_checksum(obj)
|
11
|
+
json = JSON.generate(obj)
|
12
|
+
Digest::SHA256.hexdigest(json)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -3,14 +3,16 @@
|
|
3
3
|
module CKEditor5::Rails
|
4
4
|
module Context
|
5
5
|
class Props
|
6
|
+
include CKEditor5::Rails::Concerns::Checksum
|
7
|
+
|
6
8
|
def initialize(config)
|
7
9
|
@config = config
|
8
10
|
end
|
9
11
|
|
10
12
|
def to_attributes
|
11
13
|
{
|
12
|
-
|
13
|
-
|
14
|
+
**serialized_attributes,
|
15
|
+
integrity: integrity_checksum
|
14
16
|
}
|
15
17
|
end
|
16
18
|
|
@@ -18,6 +20,19 @@ module CKEditor5::Rails
|
|
18
20
|
|
19
21
|
attr_reader :config
|
20
22
|
|
23
|
+
def integrity_checksum
|
24
|
+
unsafe_attributes = serialized_attributes.slice(:plugins)
|
25
|
+
|
26
|
+
calculate_object_checksum(unsafe_attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
def serialized_attributes
|
30
|
+
@serialized_attributes ||= {
|
31
|
+
plugins: serialize_plugins,
|
32
|
+
config: serialize_config
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
21
36
|
def serialize_plugins
|
22
37
|
(config[:plugins] || []).map { |plugin| Editor::PropsPlugin.normalize(plugin).to_h }.to_json
|
23
38
|
end
|
@@ -5,6 +5,8 @@ require_relative 'editable_height_normalizer'
|
|
5
5
|
|
6
6
|
module CKEditor5::Rails::Editor
|
7
7
|
class Props
|
8
|
+
include CKEditor5::Rails::Concerns::Checksum
|
9
|
+
|
8
10
|
EDITOR_TYPES = {
|
9
11
|
classic: 'ClassicEditor',
|
10
12
|
inline: 'InlineEditor',
|
@@ -25,8 +27,9 @@ module CKEditor5::Rails::Editor
|
|
25
27
|
|
26
28
|
def to_attributes
|
27
29
|
{
|
30
|
+
**serialized_attributes,
|
28
31
|
type: EDITOR_TYPES[@type],
|
29
|
-
|
32
|
+
integrity: integrity_checksum
|
30
33
|
}
|
31
34
|
end
|
32
35
|
|
@@ -38,14 +41,24 @@ module CKEditor5::Rails::Editor
|
|
38
41
|
|
39
42
|
attr_reader :controller_context, :watchdog, :type, :config, :editable_height
|
40
43
|
|
44
|
+
def integrity_checksum
|
45
|
+
unsafe_attributes = serialized_attributes.slice(:translations, :plugins)
|
46
|
+
|
47
|
+
calculate_object_checksum(unsafe_attributes)
|
48
|
+
end
|
49
|
+
|
41
50
|
def serialized_attributes
|
42
|
-
|
51
|
+
return @serialized_attributes if defined?(@serialized_attributes)
|
52
|
+
|
53
|
+
attributes = {
|
43
54
|
translations: serialize_translations,
|
44
55
|
plugins: serialize_plugins,
|
45
56
|
config: serialize_config,
|
46
57
|
watchdog: watchdog
|
47
58
|
}
|
48
|
-
|
59
|
+
|
60
|
+
attributes.merge!(editable_height ? { 'editable-height' => editable_height } : {})
|
61
|
+
@serialized_attributes = attributes
|
49
62
|
end
|
50
63
|
|
51
64
|
def serialize_translations
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CKEditor5::Rails
|
4
|
+
module Editor
|
5
|
+
class PropsBasePlugin
|
6
|
+
include Concerns::Checksum
|
7
|
+
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_h
|
15
|
+
raise NotImplementedError, 'This method must be implemented in a subclass'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,11 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'props_base_plugin'
|
4
|
+
|
3
5
|
module CKEditor5::Rails::Editor
|
4
|
-
class PropsInlinePlugin
|
5
|
-
attr_reader :
|
6
|
+
class PropsInlinePlugin < PropsBasePlugin
|
7
|
+
attr_reader :code
|
6
8
|
|
7
9
|
def initialize(name, code)
|
8
|
-
|
10
|
+
super(name)
|
11
|
+
|
9
12
|
@code = code
|
10
13
|
validate_code!
|
11
14
|
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
class PropsPlugin
|
5
|
-
attr_reader :name, :js_import_meta
|
3
|
+
require_relative 'props_base_plugin'
|
6
4
|
|
7
|
-
|
5
|
+
module CKEditor5::Rails::Editor
|
6
|
+
class PropsPlugin < PropsBasePlugin
|
7
|
+
attr_reader :js_import_meta
|
8
8
|
|
9
9
|
def initialize(name, premium: false, **js_import_meta)
|
10
|
+
super(name)
|
11
|
+
|
10
12
|
@name = name
|
11
13
|
@js_import_meta = if js_import_meta.empty?
|
12
14
|
{ import_name: premium ? 'ckeditor5-premium-features' : 'ckeditor5' }
|
@@ -45,7 +45,7 @@ module CKEditor5::Rails
|
|
45
45
|
def find_preset(preset)
|
46
46
|
return preset if preset.is_a?(CKEditor5::Rails::Presets::PresetBuilder)
|
47
47
|
|
48
|
-
|
48
|
+
base.presets[preset]
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
@@ -54,7 +54,8 @@ module CKEditor5::Rails
|
|
54
54
|
|
55
55
|
delegate :version, :gpl, :premium, :cdn, :translations, :license_key,
|
56
56
|
:type, :menubar, :toolbar, :plugins, :plugin, :inline_plugin,
|
57
|
-
:language, :ckbox, :configure,
|
57
|
+
:language, :ckbox, :configure, :automatic_upgrades, :simple_upload_adapter,
|
58
|
+
:editable_height, to: :default_preset
|
58
59
|
|
59
60
|
def initialize(configuration)
|
60
61
|
@configuration = configuration
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module CKEditor5::Rails
|
4
4
|
class Presets::PluginsBuilder
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :items
|
6
6
|
|
7
7
|
def initialize(plugins)
|
8
|
-
@
|
8
|
+
@items = plugins
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.create_plugin(name, **kwargs)
|
@@ -17,19 +17,19 @@ module CKEditor5::Rails
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def remove(*names)
|
20
|
-
names.each { |name|
|
20
|
+
names.each { |name| items.delete_if { |plugin| plugin.name == name } }
|
21
21
|
end
|
22
22
|
|
23
23
|
def prepend(*names, before: nil, **kwargs)
|
24
24
|
new_plugins = names.map { |name| self.class.create_plugin(name, **kwargs) }
|
25
25
|
|
26
26
|
if before
|
27
|
-
index =
|
27
|
+
index = items.index { |p| p.name == before }
|
28
28
|
raise ArgumentError, "Plugin '#{before}' not found" unless index
|
29
29
|
|
30
|
-
|
30
|
+
items.insert(index, *new_plugins)
|
31
31
|
else
|
32
|
-
|
32
|
+
items.insert(0, *new_plugins)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -37,12 +37,12 @@ module CKEditor5::Rails
|
|
37
37
|
new_plugins = names.map { |name| self.class.create_plugin(name, **kwargs) }
|
38
38
|
|
39
39
|
if after
|
40
|
-
index =
|
40
|
+
index = items.index { |p| p.name == after }
|
41
41
|
raise ArgumentError, "Plugin '#{after}' not found" unless index
|
42
42
|
|
43
|
-
|
43
|
+
items.insert(index + 1, *new_plugins)
|
44
44
|
else
|
45
|
-
|
45
|
+
items.push(*new_plugins)
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
@@ -162,10 +162,9 @@ module CKEditor5::Rails
|
|
162
162
|
}
|
163
163
|
end
|
164
164
|
|
165
|
-
return unless block
|
166
|
-
|
167
165
|
builder = ToolbarBuilder.new(@config[:toolbar][:items])
|
168
|
-
builder.instance_eval(&block)
|
166
|
+
builder.instance_eval(&block) if block_given?
|
167
|
+
builder
|
169
168
|
end
|
170
169
|
|
171
170
|
def inline_plugin(name, code)
|
@@ -183,10 +182,9 @@ module CKEditor5::Rails
|
|
183
182
|
|
184
183
|
names.each { |name| plugin(name, **kwargs) } unless names.empty?
|
185
184
|
|
186
|
-
return unless block
|
187
|
-
|
188
185
|
builder = PluginsBuilder.new(@config[:plugins])
|
189
|
-
builder.instance_eval(&block)
|
186
|
+
builder.instance_eval(&block) if block_given?
|
187
|
+
builder
|
190
188
|
end
|
191
189
|
|
192
190
|
def language(ui = nil, content: ui) # rubocop:disable Naming/MethodParameterName
|
data/lib/ckeditor5/rails.rb
CHANGED
@@ -5,6 +5,7 @@ module CKEditor5
|
|
5
5
|
require_relative 'rails/version'
|
6
6
|
require_relative 'rails/version_detector'
|
7
7
|
require_relative 'rails/semver'
|
8
|
+
require_relative 'rails/concerns/checksum'
|
8
9
|
require_relative 'rails/assets/assets_bundle'
|
9
10
|
require_relative 'rails/assets/assets_bundle_html_serializer'
|
10
11
|
require_relative 'rails/helpers'
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'e2e/spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'CKEditor5 Types Integration', type: :feature, js: true do
|
6
|
+
shared_examples 'an editor' do |path|
|
7
|
+
before { visit path }
|
8
|
+
|
9
|
+
it 'loads and initializes the editor' do
|
10
|
+
expect(page).to have_css('.ck-editor__editable', wait: 10)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
shared_examples 'an editor that fires change event with main payload' do |path|
|
15
|
+
before { visit path }
|
16
|
+
|
17
|
+
it 'sends properly change events with proper payload' do
|
18
|
+
editor = first('.ck-editor__editable')
|
19
|
+
|
20
|
+
# Set up detailed change event listener
|
21
|
+
page.execute_script(<<~JS)
|
22
|
+
window._editorEvents = [];
|
23
|
+
document.querySelector('ckeditor-component').addEventListener('editor-change', (e) => {
|
24
|
+
window._editorEvents.push({
|
25
|
+
data: e.detail.data,
|
26
|
+
hasEditor: !!e.detail.editor
|
27
|
+
});
|
28
|
+
});
|
29
|
+
JS
|
30
|
+
|
31
|
+
# Clear editor and type text
|
32
|
+
editor.click
|
33
|
+
editor.send_keys([[:control, 'a'], :backspace])
|
34
|
+
editor.send_keys('Hello from keyboard!')
|
35
|
+
|
36
|
+
# Wait for change events and verify the last one
|
37
|
+
eventually do
|
38
|
+
events = page.evaluate_script('window._editorEvents')
|
39
|
+
last_event = events.last
|
40
|
+
|
41
|
+
expect(last_event['data']).to eq('main' => '<p>Hello from keyboard!</p>')
|
42
|
+
expect(last_event['hasEditor']).to be true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
shared_examples 'a multiroot editor that fires change events' do |path, editables| # rubocop:disable Metrics/BlockLength
|
48
|
+
before { visit path }
|
49
|
+
|
50
|
+
it 'sends properly change events with proper payload for editables' do # rubocop:disable Metrics/BlockLength
|
51
|
+
editors = editables.map do |name|
|
52
|
+
find("[data-testid='#{name}-editable']")
|
53
|
+
end
|
54
|
+
|
55
|
+
# Set up detailed change event listener
|
56
|
+
page.execute_script(<<~JS)
|
57
|
+
window._editorEvents = [];
|
58
|
+
document.querySelector('ckeditor-component').addEventListener('editor-change', (e) => {
|
59
|
+
window._editorEvents.push({
|
60
|
+
data: e.detail.data,
|
61
|
+
hasEditor: !!e.detail.editor
|
62
|
+
});
|
63
|
+
});
|
64
|
+
JS
|
65
|
+
|
66
|
+
# Test each editable
|
67
|
+
expected_data = {}
|
68
|
+
editors.each_with_index do |editor, index|
|
69
|
+
editor.click
|
70
|
+
editor.send_keys([[:control, 'a'], :backspace])
|
71
|
+
content = "Content for #{editables[index]}"
|
72
|
+
editor.send_keys(content)
|
73
|
+
expected_data[editables[index]] = "<p>#{content}</p>"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Wait for change events and verify the last one
|
77
|
+
eventually do
|
78
|
+
events = page.evaluate_script('window._editorEvents')
|
79
|
+
last_event = events.last
|
80
|
+
|
81
|
+
expect(last_event['data']).to eq(expected_data)
|
82
|
+
expect(last_event['hasEditor']).to be true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe 'Classic Editor' do
|
88
|
+
it_behaves_like 'an editor', 'classic'
|
89
|
+
it_behaves_like 'an editor that fires change event with main payload', 'classic'
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'Decoupled Editor' do
|
93
|
+
before { visit 'decoupled' }
|
94
|
+
|
95
|
+
it_behaves_like 'an editor', 'decoupled'
|
96
|
+
it_behaves_like 'an editor that fires change event with main payload', 'decoupled'
|
97
|
+
|
98
|
+
it 'has separate toolbar' do
|
99
|
+
expect(page).to have_css('.toolbar-container .ck-toolbar')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'Balloon Editor' do
|
104
|
+
before { visit 'balloon' }
|
105
|
+
|
106
|
+
it_behaves_like 'an editor', 'balloon'
|
107
|
+
it_behaves_like 'an editor that fires change event with main payload', 'balloon'
|
108
|
+
|
109
|
+
it 'shows balloon toolbar on selection' do
|
110
|
+
editor = first('.ck-editor__editable')
|
111
|
+
editor.click
|
112
|
+
|
113
|
+
expect(page).to have_css('.ck-balloon-panel', wait: 5)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe 'Inline Editor' do
|
118
|
+
it_behaves_like 'an editor', 'inline'
|
119
|
+
it_behaves_like 'an editor that fires change event with main payload', 'inline'
|
120
|
+
end
|
121
|
+
|
122
|
+
describe 'Multiroot Editor' do
|
123
|
+
before { visit 'multiroot' }
|
124
|
+
|
125
|
+
it_behaves_like 'an editor', 'multiroot'
|
126
|
+
it_behaves_like 'a multiroot editor that fires change events', 'multiroot', %w[toolbar content]
|
127
|
+
|
128
|
+
it 'supports multiple editable areas' do
|
129
|
+
expect(page).to have_css('.ck-editor__editable', minimum: 2)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'shares toolbar between editables' do
|
133
|
+
expect(page).to have_css('.ck-toolbar', count: 1)
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'handles dynamically added editables' do # rubocop:disable Metrics/BlockLength
|
137
|
+
# Set up event listener
|
138
|
+
page.execute_script(<<~JS)
|
139
|
+
window._newEditableEvents = [];
|
140
|
+
document.querySelector('ckeditor-component').addEventListener('editor-change', (e) => {
|
141
|
+
window._newEditableEvents.push({
|
142
|
+
data: e.detail.data,
|
143
|
+
hasEditor: !!e.detail.editor
|
144
|
+
});
|
145
|
+
});
|
146
|
+
JS
|
147
|
+
|
148
|
+
# Add new editable component
|
149
|
+
page.execute_script(<<~JS)
|
150
|
+
const container = document.querySelector('[data-testid="multiroot-editor"]');
|
151
|
+
const newEditable = document.createElement('ckeditor-editable-component');
|
152
|
+
newEditable.setAttribute('name', 'new-root');
|
153
|
+
container.appendChild(newEditable);
|
154
|
+
JS
|
155
|
+
|
156
|
+
sleep 0.1 # Wait for component initialization
|
157
|
+
|
158
|
+
# Find and interact with new editable
|
159
|
+
new_editable = find("[name='new-root']")
|
160
|
+
new_editable.click
|
161
|
+
new_editable.send_keys('Content for new root')
|
162
|
+
|
163
|
+
# Verify the change event
|
164
|
+
eventually do
|
165
|
+
events = page.evaluate_script('window._newEditableEvents')
|
166
|
+
last_event = events.last
|
167
|
+
|
168
|
+
expect(last_event['data']).to include(
|
169
|
+
'content' => '',
|
170
|
+
'new-root' => '<p>Content for new root</p>',
|
171
|
+
'toolbar' => '<p>This is a toolbar editable</p>'
|
172
|
+
)
|
173
|
+
|
174
|
+
expect(last_event['hasEditor']).to be true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|