ckeditor5 1.15.10 → 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/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/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/spec_helper.rb +1 -1
- metadata +17 -1
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' }
|
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
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'e2e/spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'Form Integration', type: :feature, js: true do
|
6
|
+
before do
|
7
|
+
visit('form')
|
8
|
+
setup_form_tracking(page)
|
9
|
+
end
|
10
|
+
|
11
|
+
shared_examples 'a form with CKEditor' do |form_testid, editor_testid, submit_testid| # 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
|
+
|
18
|
+
it 'loads editor properly' do
|
19
|
+
expect(page).to have_css("[data-testid='#{editor_testid}'] .ck-editor__editable")
|
20
|
+
expect(editor).to have_invisible_textarea
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'validates required fields' do
|
24
|
+
editable.click
|
25
|
+
editable.send_keys([[:control, 'a'], :backspace])
|
26
|
+
|
27
|
+
text_field.set('')
|
28
|
+
submit_button.click
|
29
|
+
|
30
|
+
expect(form).not_to have_been_submitted
|
31
|
+
expect(text_field).to be_invalid
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'submits with valid data' do
|
35
|
+
editable.click
|
36
|
+
editable.send_keys('New content')
|
37
|
+
text_field.set('Second field value')
|
38
|
+
|
39
|
+
submit_button.click
|
40
|
+
|
41
|
+
eventually do
|
42
|
+
expect(form).to have_been_submitted
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'Rails form' do
|
48
|
+
it_behaves_like 'a form with CKEditor',
|
49
|
+
'rails-form',
|
50
|
+
'rails-form-editor',
|
51
|
+
'rails-form-submit'
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'Simple form' do
|
55
|
+
it_behaves_like 'a form with CKEditor',
|
56
|
+
'simple-form',
|
57
|
+
'simple-form-editor',
|
58
|
+
'simple-form-submit'
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara'
|
4
|
+
require 'capybara/rspec'
|
5
|
+
require 'capybara/cuprite'
|
6
|
+
|
7
|
+
ENV['RAILS_ENV'] ||= 'test'
|
8
|
+
|
9
|
+
require File.expand_path('../../sandbox/config/environment', __dir__)
|
10
|
+
|
11
|
+
require 'capybara/rails'
|
12
|
+
|
13
|
+
Capybara.app = Rails.application
|
14
|
+
|
15
|
+
Capybara.register_driver(:cuprite) do |app|
|
16
|
+
driver = Capybara::Cuprite::Driver.new(
|
17
|
+
app,
|
18
|
+
window_size: [1200, 800],
|
19
|
+
headless: ENV['HEADLESS'] == 'true',
|
20
|
+
browser_options: {
|
21
|
+
'no-sandbox': nil,
|
22
|
+
'disable-gpu': nil,
|
23
|
+
'enable-logging': nil
|
24
|
+
},
|
25
|
+
process_timeout: 20,
|
26
|
+
timeout: 20,
|
27
|
+
inspector: true
|
28
|
+
)
|
29
|
+
|
30
|
+
process = driver.browser.process
|
31
|
+
puts ''
|
32
|
+
puts "Browser: #{process.browser_version}"
|
33
|
+
puts "Protocol: #{process.protocol_version}"
|
34
|
+
puts "V8: #{process.v8_version}"
|
35
|
+
puts "Webkit: #{process.webkit_version}"
|
36
|
+
driver
|
37
|
+
end
|
38
|
+
|
39
|
+
Capybara.server = :webrick
|
40
|
+
Capybara.default_driver = :cuprite
|
41
|
+
Capybara.javascript_driver = :cuprite
|
42
|
+
|
43
|
+
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add eventually helper for async operations
|
4
|
+
def eventually(timeout: 5, delay: 0.1)
|
5
|
+
deadline = Time.zone.now + timeout
|
6
|
+
loop do
|
7
|
+
yield
|
8
|
+
break
|
9
|
+
rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e
|
10
|
+
raise e if Time.zone.now >= deadline
|
11
|
+
|
12
|
+
sleep delay
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FormHelpers
|
4
|
+
def setup_form_tracking(driver)
|
5
|
+
driver.execute_script <<~JS
|
6
|
+
window.lastSubmittedForm = null;
|
7
|
+
|
8
|
+
document.addEventListener('submit', (e) => {
|
9
|
+
e.preventDefault();
|
10
|
+
window.lastSubmittedForm = e.target.id;
|
11
|
+
});
|
12
|
+
JS
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.include FormHelpers, type: :feature
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec::Matchers.define :be_invalid do
|
21
|
+
match do |element|
|
22
|
+
element[:validity] == 'false' ||
|
23
|
+
element.evaluate_script('!this.validity.valid')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
RSpec::Matchers.define :have_been_submitted do
|
28
|
+
match do |form|
|
29
|
+
page.evaluate_script('window.lastSubmittedForm') == form['id']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
RSpec::Matchers.define :have_invisible_textarea do
|
34
|
+
match do |element|
|
35
|
+
element.has_css?('textarea', visible: :hidden)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe CKEditor5::Rails::Concerns::Checksum do
|
6
|
+
let(:dummy_class) do
|
7
|
+
Class.new do
|
8
|
+
include CKEditor5::Rails::Concerns::Checksum
|
9
|
+
|
10
|
+
public :calculate_object_checksum
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
subject(:instance) { dummy_class.new }
|
15
|
+
|
16
|
+
describe '#calculate_object_checksum' do
|
17
|
+
it 'returns a 16-character string' do
|
18
|
+
result = instance.calculate_object_checksum({ test: 'value' })
|
19
|
+
expect(result).to eq(
|
20
|
+
'f98be16ebfa861cb39a61faff9e52b33f5bcc16bb6ae72e728d226dc07093932'
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns consistent checksums for the same input' do
|
25
|
+
input = { name: 'test', value: 123 }
|
26
|
+
first_result = instance.calculate_object_checksum(input)
|
27
|
+
second_result = instance.calculate_object_checksum(input)
|
28
|
+
expect(first_result).to eq(second_result)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns different checksums for different inputs' do
|
32
|
+
result1 = instance.calculate_object_checksum({ a: 1 })
|
33
|
+
result2 = instance.calculate_object_checksum({ a: 2 })
|
34
|
+
expect(result1).not_to eq(result2)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'handles arrays' do
|
38
|
+
result = instance.calculate_object_checksum([1, 2, 3])
|
39
|
+
expect(result).to eq(
|
40
|
+
'a615eeaee21de5179de080de8c3052c8da901138406ba71c38c032845f7d54f4'
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'is order dependent for hashes' do
|
45
|
+
result1 = instance.calculate_object_checksum({ a: 1, b: 2 })
|
46
|
+
result2 = instance.calculate_object_checksum({ b: 2, a: 1 })
|
47
|
+
expect(result1).not_to eq(result2)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -25,9 +25,15 @@ RSpec.describe CKEditor5::Rails::Context::Props do
|
|
25
25
|
describe '#to_attributes' do
|
26
26
|
subject(:attributes) { props.to_attributes }
|
27
27
|
|
28
|
+
it 'returns integrity property' do
|
29
|
+
expect(attributes[:integrity]).to eq(
|
30
|
+
'24e46c3ee19f6764930b38ecdf62c0ac824a0acbe6616b46199d892afb211acb'
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
28
34
|
it 'returns a hash with plugins and config keys' do
|
29
35
|
expect(attributes).to be_a(Hash)
|
30
|
-
expect(attributes.keys).to match_array(%i[plugins config])
|
36
|
+
expect(attributes.keys).to match_array(%i[plugins integrity config])
|
31
37
|
end
|
32
38
|
|
33
39
|
describe ':plugins key' do
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe CKEditor5::Rails::Editor::PropsBasePlugin do
|
6
|
+
let(:concrete_class) do
|
7
|
+
Class.new(described_class) do
|
8
|
+
def to_unsafe_h
|
9
|
+
{ type: :test, name: name }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:instance) { concrete_class.new(:TestPlugin) }
|
15
|
+
|
16
|
+
describe '#initialize' do
|
17
|
+
it 'sets the name attribute' do
|
18
|
+
expect(instance.name).to eq(:TestPlugin)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#to_h' do
|
23
|
+
it 'raises NotImplementedError' do
|
24
|
+
expect { instance.to_h }.to raise_error(NotImplementedError)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -28,11 +28,13 @@ RSpec.describe CKEditor5::Rails::Editor::Props do
|
|
28
28
|
|
29
29
|
it 'includes required attributes' do
|
30
30
|
attributes = props.to_attributes
|
31
|
+
|
31
32
|
expect(attributes).to include(
|
32
33
|
type: 'ClassicEditor',
|
33
34
|
translations: String,
|
34
35
|
plugins: String,
|
35
36
|
config: String,
|
37
|
+
integrity: '358d88b83d041f208d94ac957b2fd68135f1caab5c0d101d33cf04d5d39d81ef',
|
36
38
|
watchdog: true
|
37
39
|
)
|
38
40
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -36,7 +36,7 @@ require 'rspec-html-matchers'
|
|
36
36
|
|
37
37
|
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
|
38
38
|
|
39
|
-
Rails.application.initialize!
|
39
|
+
Rails.application.initialize! unless Rails.application.initialized?
|
40
40
|
|
41
41
|
RSpec.configure do |config|
|
42
42
|
config.expect_with :rspec do |expectations|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ckeditor5
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mateusz Bagiński
|
@@ -55,6 +55,7 @@ files:
|
|
55
55
|
- lib/ckeditor5/rails/cdn/ckeditor_bundle.rb
|
56
56
|
- lib/ckeditor5/rails/cdn/helpers.rb
|
57
57
|
- lib/ckeditor5/rails/cdn/url_generator.rb
|
58
|
+
- lib/ckeditor5/rails/concerns/checksum.rb
|
58
59
|
- lib/ckeditor5/rails/context/helpers.rb
|
59
60
|
- lib/ckeditor5/rails/context/props.rb
|
60
61
|
- lib/ckeditor5/rails/editor/editable_height_normalizer.rb
|
@@ -62,6 +63,7 @@ files:
|
|
62
63
|
- lib/ckeditor5/rails/editor/helpers/config_helpers.rb
|
63
64
|
- lib/ckeditor5/rails/editor/helpers/editor_helpers.rb
|
64
65
|
- lib/ckeditor5/rails/editor/props.rb
|
66
|
+
- lib/ckeditor5/rails/editor/props_base_plugin.rb
|
65
67
|
- lib/ckeditor5/rails/editor/props_inline_plugin.rb
|
66
68
|
- lib/ckeditor5/rails/editor/props_plugin.rb
|
67
69
|
- lib/ckeditor5/rails/engine.rb
|
@@ -76,17 +78,24 @@ files:
|
|
76
78
|
- lib/ckeditor5/rails/semver.rb
|
77
79
|
- lib/ckeditor5/rails/version.rb
|
78
80
|
- lib/ckeditor5/rails/version_detector.rb
|
81
|
+
- spec/e2e/features/editor_types_spec.rb
|
82
|
+
- spec/e2e/features/form_integration_spec.rb
|
83
|
+
- spec/e2e/spec_helper.rb
|
84
|
+
- spec/e2e/support/eventually.rb
|
85
|
+
- spec/e2e/support/form_helpers.rb
|
79
86
|
- spec/lib/ckeditor5/rails/assets/asset_bundle_hml_serializer_spec.rb
|
80
87
|
- spec/lib/ckeditor5/rails/assets/assets_bundle_spec.rb
|
81
88
|
- spec/lib/ckeditor5/rails/cdn/ckbox_bundle_spec.rb
|
82
89
|
- spec/lib/ckeditor5/rails/cdn/ckeditor_bundle_spec.rb
|
83
90
|
- spec/lib/ckeditor5/rails/cdn/helpers_spec.rb
|
84
91
|
- spec/lib/ckeditor5/rails/cdn/url_generator_spec.rb
|
92
|
+
- spec/lib/ckeditor5/rails/concerns/checksum_spec.rb
|
85
93
|
- spec/lib/ckeditor5/rails/context/helpers_spec.rb
|
86
94
|
- spec/lib/ckeditor5/rails/context/props_spec.rb
|
87
95
|
- spec/lib/ckeditor5/rails/editor/editable_height_normalizer_spec.rb
|
88
96
|
- spec/lib/ckeditor5/rails/editor/helpers/config_helpers_spec.rb
|
89
97
|
- spec/lib/ckeditor5/rails/editor/helpers/editor_helpers_spec.rb
|
98
|
+
- spec/lib/ckeditor5/rails/editor/props_base_plugin_spec.rb
|
90
99
|
- spec/lib/ckeditor5/rails/editor/props_inline_plugin_spec.rb
|
91
100
|
- spec/lib/ckeditor5/rails/editor/props_plugin_spec.rb
|
92
101
|
- spec/lib/ckeditor5/rails/editor/props_spec.rb
|
@@ -126,17 +135,24 @@ signing_key:
|
|
126
135
|
specification_version: 4
|
127
136
|
summary: CKEditor 5 for Rails
|
128
137
|
test_files:
|
138
|
+
- spec/e2e/features/editor_types_spec.rb
|
139
|
+
- spec/e2e/features/form_integration_spec.rb
|
140
|
+
- spec/e2e/spec_helper.rb
|
141
|
+
- spec/e2e/support/eventually.rb
|
142
|
+
- spec/e2e/support/form_helpers.rb
|
129
143
|
- spec/lib/ckeditor5/rails/assets/asset_bundle_hml_serializer_spec.rb
|
130
144
|
- spec/lib/ckeditor5/rails/assets/assets_bundle_spec.rb
|
131
145
|
- spec/lib/ckeditor5/rails/cdn/ckbox_bundle_spec.rb
|
132
146
|
- spec/lib/ckeditor5/rails/cdn/ckeditor_bundle_spec.rb
|
133
147
|
- spec/lib/ckeditor5/rails/cdn/helpers_spec.rb
|
134
148
|
- spec/lib/ckeditor5/rails/cdn/url_generator_spec.rb
|
149
|
+
- spec/lib/ckeditor5/rails/concerns/checksum_spec.rb
|
135
150
|
- spec/lib/ckeditor5/rails/context/helpers_spec.rb
|
136
151
|
- spec/lib/ckeditor5/rails/context/props_spec.rb
|
137
152
|
- spec/lib/ckeditor5/rails/editor/editable_height_normalizer_spec.rb
|
138
153
|
- spec/lib/ckeditor5/rails/editor/helpers/config_helpers_spec.rb
|
139
154
|
- spec/lib/ckeditor5/rails/editor/helpers/editor_helpers_spec.rb
|
155
|
+
- spec/lib/ckeditor5/rails/editor/props_base_plugin_spec.rb
|
140
156
|
- spec/lib/ckeditor5/rails/editor/props_inline_plugin_spec.rb
|
141
157
|
- spec/lib/ckeditor5/rails/editor/props_plugin_spec.rb
|
142
158
|
- spec/lib/ckeditor5/rails/editor/props_spec.rb
|