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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1cdcc72197788bc6dc57bb3b01158c932dbf27621084414bc832c5d772e01701
4
- data.tar.gz: 7d6874e08d93346f9d478f46e2240bbc5df78a32700f50087de8fe8e4301cfc8
3
+ metadata.gz: e91fe2d11179c703901756efa978fcedcd3030d0381c4e550ab8c9c15ef73a19
4
+ data.tar.gz: 4119948cdae51969b3d1e77801f20faecd81cfbf2755388fc444421e7da8bdb9
5
5
  SHA512:
6
- metadata.gz: 7dc9311974b2dd72a05fd61cea086585179e64700f800618d06f4bffd079f31ad99d31f1ea5917d7c28cdd4e03d675122b546d25518ed43a99928f2061afd448
7
- data.tar.gz: 199da996b61646c658dbbe0c8119419220b669442a5eba6fc1c973dad7793e3db8e3a1e8808e075e8d0477236fd8741fa2927eae9c141fc7bb87364df6fee7ae
6
+ metadata.gz: d4f236d5bc883344daa8fa9bc0b7d55e3898360dd8fabfd48265bbd1eedf3c654e8ba6baf5b0a1ba5ffa215c6b2c14a60c02f7182b81885a875b0a4902f67746
7
+ data.tar.gz: 593de9199583c1f22a38b9858967eca63b9c98eef8b7a6a1e5a06ccfa1df759424dab96ed88c1719614744b26c8a32920722156875ba748e66c80272eeb0fd78
data/Gemfile CHANGED
@@ -24,6 +24,7 @@ end
24
24
 
25
25
  group :test, :development do
26
26
  gem 'capybara', '~> 3.40'
27
+ gem 'cuprite', '~> 0.15.0'
27
28
  gem 'rspec', '~> 3.13'
28
29
  gem 'rspec-expectations', '~> 3.13'
29
30
  gem 'rspec-html-matchers', '~> 0.10.0'
@@ -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(imports.map(item => {
70
- switch(item.type) {
71
- case 'inline':
72
- return loadInlinePlugin(item);
73
-
74
- case 'external':
75
- default:
76
- return loadExternalPlugin(item);
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
- plugins: serialize_plugins,
13
- config: serialize_config
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
- **serialized_attributes
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
- .merge(editable_height ? { 'editable-height' => editable_height } : {})
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 :name, :code
6
+ class PropsInlinePlugin < PropsBasePlugin
7
+ attr_reader :code
6
8
 
7
9
  def initialize(name, code)
8
- @name = name
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
- module CKEditor5::Rails::Editor
4
- class PropsPlugin
5
- attr_reader :name, :js_import_meta
3
+ require_relative 'props_base_plugin'
6
4
 
7
- delegate :to_h, to: :import_meta
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' }
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CKEditor5
4
4
  module Rails
5
- VERSION = '1.15.10'
5
+ VERSION = '1.16.0'
6
6
 
7
7
  DEFAULT_CKEDITOR_VERSION = '43.3.1'
8
8
  end
@@ -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.15.10
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