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 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