# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CKEditor5::Rails::Cdn::Helpers do
let(:test_class) do
Class.new do
include CKEditor5::Rails::Cdn::Helpers
def importmap_rendered?
false
end
def content_security_policy_nonce
'test-nonce'
end
end
end
let(:helper) { test_class.new }
let(:preset) do
CKEditor5::Rails::Presets::PresetBuilder.new do
version '34.1.0'
type :classic
translations :pl
cdn :cloud
license_key nil
premium false
end
end
let(:context) do
helper.instance_variable_get(:@__ckeditor_context)
end
let(:bundle_html) { ''.html_safe }
let(:serializer) do
instance_double(CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer, to_html: bundle_html)
end
before do
allow(CKEditor5::Rails::Engine).to receive(:find_preset!).and_return(preset)
allow(CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer).to receive(:new).and_return(serializer)
end
after do
RSpec::Mocks.space.proxy_for(CKEditor5::Rails::Engine).reset
RSpec::Mocks.space.proxy_for(CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer).reset
end
describe '#ckeditor5_assets' do
context 'with valid preset' do
it 'creates base bundle' do
expect(CKEditor5::Rails::Cdn::CKEditorBundle).to receive(:new)
.with(
instance_of(CKEditor5::Rails::Semver),
'ckeditor5',
translations: %i[pl en],
cdn: :cloud
)
.and_call_original
helper.ckeditor5_assets(preset: :default)
end
context 'with premium features' do
let(:preset) do
CKEditor5::Rails::Presets::PresetBuilder.new do
version '34.1.0'
type :classic
translations :pl
cdn :cloud
premium true
end
end
it 'creates base and premium bundles' do
expect(CKEditor5::Rails::Cdn::CKEditorBundle).to receive(:new)
.with(
instance_of(CKEditor5::Rails::Semver),
'ckeditor5',
translations: %i[pl en],
cdn: :cloud
)
.and_call_original
.ordered
expect(CKEditor5::Rails::Cdn::CKEditorBundle).to receive(:new)
.with(
instance_of(CKEditor5::Rails::Semver),
'ckeditor5-premium-features',
translations: %i[pl en],
cdn: :cloud
)
.and_call_original
.ordered
helper.ckeditor5_assets(preset: :default)
end
end
context 'with ckbox' do
let(:preset) do
CKEditor5::Rails::Presets::PresetBuilder.new do
version '34.1.0'
type :classic
translations :pl
cdn :cloud
ckbox '1.0.0', theme: :lark
end
end
it 'creates ckbox bundle' do
expect(CKEditor5::Rails::Cdn::CKBoxBundle).to receive(:new)
.with(
instance_of(CKEditor5::Rails::Semver),
theme: :lark,
cdn: :ckbox
)
.and_call_original
helper.ckeditor5_assets(preset: :default)
end
end
context 'with plugins having preload assets' do
let(:plugin_bundle) { CKEditor5::Rails::Assets::AssetsBundle.new(scripts: ['plugin.js']) }
let(:plugin) { instance_double('Plugin', preload_assets_bundle: plugin_bundle) }
let(:plugin_without_preload) { instance_double('Plugin', preload_assets_bundle: nil) }
before do
allow(preset).to receive_message_chain(:plugins, :items)
.and_return([plugin, plugin_without_preload])
end
it 'includes plugin preload assets in the bundle' do
helper.ckeditor5_assets(preset: :default)
expect(context[:bundle].scripts).to include('plugin.js')
end
it 'merges plugin assets with the main bundle' do
expect(serializer).to receive(:to_html)
helper.ckeditor5_assets(preset: :default)
bundle = context[:bundle]
expect(bundle.scripts).to include('plugin.js')
end
end
end
context 'when overriding preset values' do
let(:preset) do
CKEditor5::Rails::Presets::PresetBuilder.new do
version '34.1.0'
type :classic
language :pl
cdn :cloud
license_key 'preset-license'
premium false
end
end
it 'allows overriding preset values' do
helper.ckeditor5_assets(preset: :default, license_key: 'overridden-license')
expect(context[:preset].license_key).to eq('overridden-license')
end
it 'preserves non-overridden preset values' do
helper.ckeditor5_assets(preset: :default, license_key: 'overridden-license')
preset_context = context[:preset]
expect(preset_context.version).to eq('34.1.0')
expect(preset_context.premium?).to be false
expect(preset_context.cdn).to eq(:cloud)
expect(preset_context.translations).to eq(%i[en pl])
expect(preset_context.type).to eq(:classic)
end
it 'allows to override language using language parameter' do
preset.language(:en)
helper.ckeditor5_assets(preset: :default, language: :pl)
expect(context[:preset].language).to eq({ ui: :pl, content: :pl })
end
it 'should not override language if it\'s specified in preset and not passed to helper' do
preset.language(:en)
helper.ckeditor5_assets(preset: :default)
expect(context[:preset].language).to eq({ ui: :en, content: :en })
end
it 'should use I18n.locale as default language if it\'s not specified in preset' do
preset.configure :language, nil
allow(I18n).to receive(:locale).and_return(:pl)
helper.ckeditor5_assets(preset: :default)
expect(context[:preset].language).to eq({ ui: :pl, content: :pl })
end
end
context 'with missing required parameters' do
before do
allow(helper).to receive(:merge_with_editor_preset).and_return({})
end
it 'raises error about missing required parameters' do
expect { helper.ckeditor5_assets(preset: :default) }
.to raise_error(NoMatchingPatternKeyError)
end
end
context 'destructure non-matching preset override' do
before do
RSpec::Mocks.space.proxy_for(CKEditor5::Rails::Engine).reset
end
it 'raises error' do
expect { helper.ckeditor5_assets(preset: :invalid) }
.to raise_error(CKEditor5::Rails::PresetNotFoundError)
RSpec::Mocks.space.proxy_for(CKEditor5::Rails::Engine).reset
end
end
context 'with empty preset' do
let(:preset) { CKEditor5::Rails::Presets::PresetBuilder.new }
it 'raises error about missing version and type' do
expect { helper.ckeditor5_assets(preset: :default) }
.to raise_error(ArgumentError, /forgot to define version/)
end
end
context 'when Rails.application.importmap is defined' do
before do
allow(helper).to receive(:importmap_available?).and_return(true)
allow(helper).to receive(:importmap_rendered?).and_return(false)
end
it 'returns nil and stores html tags in context' do
result = helper.ckeditor5_assets(preset: :default)
expect(result).to be_nil
expect(context[:html_tags]).to eq(bundle_html)
end
it 'raise exception if importmap_rendered?' do
allow(helper).to receive(:importmap_rendered?).and_return(true)
expect { helper.ckeditor5_assets(preset: :default) }
.to raise_error(CKEditor5::Rails::Cdn::Helpers::ImportmapAlreadyRenderedError)
end
end
context 'when importmap_available? is true returns html' do
before do
allow(helper).to receive(:importmap_available?).and_return(nil)
end
it 'returns html directly' do
result = helper.ckeditor5_assets(preset: :default)
expect(result).to eq(bundle_html)
expect(context[:html_tags]).to be_nil
end
end
end
describe '#ckeditor5_lazy_javascript_tags' do
let(:web_component_html) do
''.html_safe
end
let(:import_map_html) { ''.html_safe }
let(:web_component_bundle) do
instance_double(CKEditor5::Rails::Assets::WebComponentBundle, to_html: web_component_html)
end
let(:import_map_bundle) do
instance_double(CKEditor5::Rails::Assets::AssetsImportMap, to_html: import_map_html)
end
let(:preset_manager) { instance_double(CKEditor5::Rails::Presets::Manager) }
let(:test_preset1) { instance_double(CKEditor5::Rails::Presets::PresetBuilder) }
let(:test_preset2) { instance_double(CKEditor5::Rails::Presets::PresetBuilder) }
before do
allow(CKEditor5::Rails::Assets::WebComponentBundle).to receive(:instance).and_return(
web_component_bundle
)
allow(CKEditor5::Rails::Assets::AssetsImportMap).to receive(:new).and_return(
import_map_bundle
)
allow(CKEditor5::Rails::Engine).to receive(:presets).and_return(preset_manager)
allow(preset_manager).to receive(:to_h).and_return({
test1: test_preset1,
test2: test_preset2
})
allow(helper).to receive(:create_preset_bundle).with(test_preset1)
.and_return(CKEditor5::Rails::Assets::AssetsBundle.new(
scripts: ['test1.js']
))
allow(helper).to receive(:create_preset_bundle).with(test_preset2)
.and_return(CKEditor5::Rails::Assets::AssetsBundle.new(
scripts: ['test2.js']
))
allow(test_preset1).to receive(:plugins).and_return(
instance_double('PluginsBuilder', items: [])
)
allow(test_preset2).to receive(:plugins).and_return(
instance_double('PluginsBuilder', items: [])
)
end
context 'when importmap is available' do
before do
allow(helper).to receive(:importmap_available?).and_return(true)
allow(helper).to receive(:importmap_rendered?).and_return(false)
end
it 'stores bundle in context and returns web component script' do
result = helper.ckeditor5_lazy_javascript_tags.html_safe
expect(result).to have_tag('script', with: {
type: 'module',
src: 'web-component.js'
})
expect(context[:bundle].scripts).to match_array(['test1.js', 'test2.js'])
end
it 'raises error when importmap is already rendered' do
allow(helper).to receive(:importmap_rendered?).and_return(true)
expect { helper.ckeditor5_lazy_javascript_tags }
.to raise_error(CKEditor5::Rails::Cdn::Helpers::ImportmapAlreadyRenderedError)
end
end
context 'when importmap is not available' do
before do
allow(helper).to receive(:importmap_available?).and_return(false)
end
it 'returns both importmap and web component scripts as one string' do
result = helper.ckeditor5_lazy_javascript_tags
expect(result).to have_tag('script', with: { type: 'importmap' },
text: '{"imports":{}}')
expect(result).to have_tag('script', with: {
type: 'module',
src: 'web-component.js'
})
end
end
end
describe '#ckeditor5_inline_plugins_tags' do
let(:preset) do
CKEditor5::Rails::Presets::PresetBuilder.new do
inline_plugin 'Plugin1', <<~JAVASCRIPT
const { Plugin } = await import( 'ckeditor5' );
return class Plugin1 extends Plugin {
init() {
window.Plugin1 = true;
}
}
JAVASCRIPT
inline_plugin 'Plugin2', <<~JAVASCRIPT
const { Plugin } = await import( 'ckeditor5' );
return class Plugin2 extends Plugin {
init() {
window.Plugin2 = true;
}
}
JAVASCRIPT
end
end
let(:another_preset) do
CKEditor5::Rails::Presets::PresetBuilder.new do
inline_plugin 'Plugin3', <<~JAVASCRIPT
const { Plugin } = await import( 'ckeditor5' );
return class Plugin3 extends Plugin {
init() {
window.Plugin3 = true;
}
}
JAVASCRIPT
end
end
before do
allow(CKEditor5::Rails::Engine).to receive(:presets).and_return(
double('PresetManager', to_h: { default: preset, another: another_preset })
)
end
it 'generates script tags for inline plugins from given preset' do
result = helper.ckeditor5_inline_plugins_tags(preset)
expect(result).to have_tag('script', count: 2)
expect(result).to include('window.Plugin1=true')
expect(result).to include('window.Plugin2=true')
expect(result).not_to include('window.Plugin3=true')
end
it 'generates script tags for inline plugins from all presets when no preset given' do
result = helper.ckeditor5_inline_plugins_tags
expect(result).to have_tag('script', count: 3)
expect(result).to include('window.Plugin1=true')
expect(result).to include('window.Plugin2=true')
expect(result).to include('window.Plugin3=true')
end
it 'adds nonce to script tags when available' do
result = helper.ckeditor5_inline_plugins_tags(preset)
expect(result).to have_tag('script', with: { nonce: 'test-nonce' })
end
context 'event listener' do
it 'adds event listeners for ckeditor:request-cjs-plugin' do
result = helper.ckeditor5_inline_plugins_tags(preset)
expect(result).to include("window.addEventListener('ckeditor:request-cjs-plugin:Plugin1'")
expect(result).to include("window.addEventListener('ckeditor:request-cjs-plugin:Plugin2'")
end
it 'adds event listeners only once' do
result = helper.ckeditor5_inline_plugins_tags(preset)
expect(result.scan("window.addEventListener('ckeditor:request-cjs-plugin:Plugin1'").count).to eq(1)
expect(result.scan("window.addEventListener('ckeditor:request-cjs-plugin:Plugin2'").count).to eq(1)
end
it('each event listener has once option') do
result = helper.ckeditor5_inline_plugins_tags(preset)
expect(result).to include('{ once: true }')
expect(result.scan('{ once: true }').length).to eq(2)
end
end
context 'with preset having no inline plugins' do
let(:empty_preset) do
CKEditor5::Rails::Presets::PresetBuilder.new do
plugins :Bold, :Italic # Regular plugins, not inline
end
end
it 'returns empty safe buffer when no inline plugins are present' do
result = helper.ckeditor5_inline_plugins_tags(empty_preset)
expect(result).to be_html_safe
expect(result).to be_empty
end
end
context 'with nil preset' do
it 'includes plugins from all registered presets' do
result = helper.ckeditor5_inline_plugins_tags(nil)
expect(result).to have_tag('script', count: 3)
expect(result).to include('window.Plugin1=true')
expect(result).to include('window.Plugin2=true')
expect(result).to include('window.Plugin3=true')
end
end
end
describe 'cdn helper methods' do
it 'generates helper methods for third-party CDNs' do
expect(helper).to respond_to(:ckeditor5_unpkg_assets)
expect(helper).to respond_to(:ckeditor5_jsdelivr_assets)
end
it 'calls main helper with proper cdn parameter' do
expect(helper).to receive(:ckeditor5_assets).with(cdn: :unpkg, version: '34.1.0')
helper.ckeditor5_unpkg_assets(version: '34.1.0')
end
end
end