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