Initial Drupal 11 with DDEV setup
This commit is contained in:
51
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml
vendored
Normal file
51
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- filter.format.basic_html
|
||||
# TRICKY: This technically is a module that this config depends on, but it has been removed from Drupal >=10.
|
||||
# module:
|
||||
# - ckeditor
|
||||
format: basic_html
|
||||
editor: ckeditor
|
||||
settings:
|
||||
toolbar:
|
||||
rows:
|
||||
-
|
||||
-
|
||||
name: Formatting
|
||||
items:
|
||||
- Bold
|
||||
- Italic
|
||||
-
|
||||
name: Linking
|
||||
items:
|
||||
- DrupalLink
|
||||
- DrupalUnlink
|
||||
-
|
||||
name: Lists
|
||||
items:
|
||||
- BulletedList
|
||||
- NumberedList
|
||||
-
|
||||
name: Media
|
||||
items:
|
||||
- Blockquote
|
||||
- DrupalImage
|
||||
-
|
||||
name: 'Block Formatting'
|
||||
items:
|
||||
- Format
|
||||
-
|
||||
name: Tools
|
||||
items:
|
||||
- Source
|
||||
plugins: {}
|
||||
image_upload:
|
||||
status: true
|
||||
scheme: public
|
||||
directory: inline-images
|
||||
max_size: null
|
||||
max_dimensions:
|
||||
width: null
|
||||
height: null
|
||||
59
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.full_html.yml
vendored
Normal file
59
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.full_html.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- filter.format.full_html
|
||||
# TRICKY: This technically is a module that this config depends on, but it has been removed from Drupal >=10.
|
||||
# module:
|
||||
# - ckeditor
|
||||
format: full_html
|
||||
editor: ckeditor
|
||||
settings:
|
||||
toolbar:
|
||||
rows:
|
||||
-
|
||||
-
|
||||
name: Formatting
|
||||
items:
|
||||
- Bold
|
||||
- Italic
|
||||
- Strike
|
||||
- Superscript
|
||||
- Subscript
|
||||
- '-'
|
||||
- RemoveFormat
|
||||
-
|
||||
name: Linking
|
||||
items:
|
||||
- DrupalLink
|
||||
- DrupalUnlink
|
||||
-
|
||||
name: Lists
|
||||
items:
|
||||
- BulletedList
|
||||
- NumberedList
|
||||
-
|
||||
name: Media
|
||||
items:
|
||||
- Blockquote
|
||||
- DrupalImage
|
||||
- Table
|
||||
- HorizontalRule
|
||||
-
|
||||
name: 'Block Formatting'
|
||||
items:
|
||||
- Format
|
||||
-
|
||||
name: Tools
|
||||
items:
|
||||
- ShowBlocks
|
||||
- Source
|
||||
plugins: {}
|
||||
image_upload:
|
||||
status: true
|
||||
scheme: public
|
||||
directory: inline-images
|
||||
max_size: null
|
||||
max_dimensions:
|
||||
width: null
|
||||
height: null
|
||||
44
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml
vendored
Normal file
44
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- editor
|
||||
name: 'Basic HTML'
|
||||
format: basic_html
|
||||
weight: 0
|
||||
roles:
|
||||
- authenticated
|
||||
filters:
|
||||
editor_file_reference:
|
||||
id: editor_file_reference
|
||||
provider: editor
|
||||
status: true
|
||||
weight: 11
|
||||
settings: { }
|
||||
filter_align:
|
||||
id: filter_align
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 7
|
||||
settings: { }
|
||||
filter_caption:
|
||||
id: filter_caption
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 8
|
||||
settings: { }
|
||||
filter_html:
|
||||
id: filter_html
|
||||
provider: filter
|
||||
status: true
|
||||
weight: -10
|
||||
settings:
|
||||
allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <p> <br> <span> <img src alt height width data-entity-type data-entity-uuid data-align data-caption>'
|
||||
filter_html_help: false
|
||||
filter_html_nofollow: false
|
||||
filter_html_image_secure:
|
||||
id: filter_html_image_secure
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 9
|
||||
settings: { }
|
||||
35
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.full_html.yml
vendored
Normal file
35
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.full_html.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- editor
|
||||
name: 'Full HTML'
|
||||
format: full_html
|
||||
weight: 2
|
||||
roles:
|
||||
- administrator
|
||||
filters:
|
||||
filter_align:
|
||||
id: filter_align
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 8
|
||||
settings: { }
|
||||
filter_caption:
|
||||
id: filter_caption
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 9
|
||||
settings: { }
|
||||
filter_htmlcorrector:
|
||||
id: filter_htmlcorrector
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 10
|
||||
settings: { }
|
||||
editor_file_reference:
|
||||
id: editor_file_reference
|
||||
provider: editor
|
||||
status: true
|
||||
weight: 11
|
||||
settings: { }
|
||||
31
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.restricted_html.yml
vendored
Normal file
31
web/core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.restricted_html.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
name: 'Restricted HTML'
|
||||
format: restricted_html
|
||||
weight: 1
|
||||
roles:
|
||||
- anonymous
|
||||
filters:
|
||||
filter_html:
|
||||
id: filter_html
|
||||
provider: filter
|
||||
status: true
|
||||
weight: -10
|
||||
settings:
|
||||
allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>'
|
||||
filter_html_help: true
|
||||
filter_html_nofollow: false
|
||||
filter_autop:
|
||||
id: filter_autop
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 0
|
||||
settings: { }
|
||||
filter_url:
|
||||
id: filter_url
|
||||
provider: filter
|
||||
status: true
|
||||
weight: 0
|
||||
settings:
|
||||
filter_url_length: 72
|
||||
1
web/core/modules/ckeditor5/tests/fixtures/test-svg-upload.svg
vendored
Normal file
1
web/core/modules/ckeditor5/tests/fixtures/test-svg-upload.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#ffd23f"><path d="M6.5 1h3v9h-3z"/><circle cx="8" cy="13.5" r="1.5"/></g></svg>
|
||||
|
After Width: | Height: | Size: 164 B |
@ -0,0 +1,17 @@
|
||||
ckeditor5_automatic_link_decorator_test_llamaClass:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
config:
|
||||
link:
|
||||
decorators:
|
||||
pinkColor:
|
||||
mode: 'automatic'
|
||||
attributes:
|
||||
class: 'llama'
|
||||
drupal:
|
||||
label: Links must have 'llama' class!
|
||||
elements:
|
||||
- <a class>
|
||||
conditions:
|
||||
plugins:
|
||||
- ckeditor5_link
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Automatic Link Decorator Test
|
||||
type: module
|
||||
description: "Provides infrastructure for testing CKEditor 5 automatic link decorators."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,13 @@
|
||||
ckeditor5_automatic_link_decorator_test_2_addTargetToExternalLinks:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
config:
|
||||
link:
|
||||
addTargetToExternalLinks: true
|
||||
drupal:
|
||||
label: Open external links in a new tab
|
||||
elements:
|
||||
- <a target="_blank" rel="noopener noreferrer">
|
||||
conditions:
|
||||
plugins:
|
||||
- ckeditor5_link
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Automatic Link Decorator Test (External links)
|
||||
type: module
|
||||
description: "Provides infrastructure for testing CKEditor 5 external links automatic link decorator."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,111 @@
|
||||
ckeditor5_definition_supporting_element_just_nav:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <nav>
|
||||
elements:
|
||||
- <nav>
|
||||
|
||||
ckeditor5_definition_supporting_element_just_article:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <article>
|
||||
elements:
|
||||
- <article>
|
||||
|
||||
ckeditor5_definition_supporting_element_article_class:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <article class>
|
||||
elements:
|
||||
- <article class>
|
||||
|
||||
ckeditor5_definition_supporting_element_article_class_with_values:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <article class="…">
|
||||
elements:
|
||||
- <article class="this-value that-value">
|
||||
|
||||
ckeditor5_definition_supporting_element_just_footer:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <footer>
|
||||
elements:
|
||||
- <footer>
|
||||
|
||||
ckeditor5_definition_supporting_element_footer_class:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <footer class>
|
||||
elements:
|
||||
- <footer class>
|
||||
|
||||
ckeditor5_definition_supporting_element_just_aside:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <aside>
|
||||
elements:
|
||||
- <aside>
|
||||
|
||||
ckeditor5_definition_supporting_element_aside_class_with_values:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <article class="…">
|
||||
elements:
|
||||
- <aside class="this-value that-value">
|
||||
|
||||
ckeditor5_definition_supporting_element_main_class:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <main class>
|
||||
elements:
|
||||
- <main class>
|
||||
|
||||
ckeditor5_definition_supporting_element_main_class_with_values:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <main class="…">
|
||||
elements:
|
||||
- <main class="this-value that-value">
|
||||
|
||||
ckeditor5_definition_supporting_element_figure_one_attrib:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <figure data-one>
|
||||
elements:
|
||||
- <figure data-one>
|
||||
|
||||
ckeditor5_definition_supporting_element_figure_two_attrib:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <figure data-one data-two>
|
||||
elements:
|
||||
- <figure data-one data-two>
|
||||
|
||||
ckeditor5_definition_supporting_element_dialog_two_attrib:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <dialog data-one data-two>
|
||||
elements:
|
||||
- <dialog data-one data-two>
|
||||
|
||||
ckeditor5_definition_supporting_element_dialog_one_attrib:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — <dialog data-one>
|
||||
elements:
|
||||
- <dialog data-one>
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Definition Supporting Tags Test
|
||||
type: module
|
||||
description: "Provides test plugins for CKEditor 5 to test finding plugin definitions that support specific tags."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,7 @@
|
||||
name: CKEditor 5 Drupal Element Style Test
|
||||
type: module
|
||||
description: "Provides ability to run DrupalElementStyle CKEditor 5 plugin in multiple ways."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- drupal:ckeditor5
|
||||
- drupal:media
|
||||
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor5_drupalelementstyle_test\Hook;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
// cspell:ignore drupalelementstyle
|
||||
|
||||
/**
|
||||
* Hook implementations for ckeditor5_drupalelementstyle_test.
|
||||
*/
|
||||
class Ckeditor5DrupalElementStyleTestHooks {
|
||||
|
||||
/**
|
||||
* Implements hook_ckeditor5_plugin_info_alter().
|
||||
*/
|
||||
#[Hook('ckeditor5_plugin_info_alter')]
|
||||
public function ckeditor5PluginInfoAlter(array &$plugin_definitions): void {
|
||||
// Update `media_mediaAlign`.
|
||||
assert($plugin_definitions['media_mediaAlign'] instanceof CKEditor5PluginDefinition);
|
||||
$media_align_plugin_definition = $plugin_definitions['media_mediaAlign']->toArray();
|
||||
$media_align_plugin_definition['ckeditor5']['config']['drupalMedia']['toolbar'] = [
|
||||
0 => [
|
||||
'name' => 'drupalMedia:align',
|
||||
'title' => 'Test title',
|
||||
'display' => 'splitButton',
|
||||
'items' => array_values(array_filter($media_align_plugin_definition['ckeditor5']['config']['drupalMedia']['toolbar'], function (string $toolbar_item): bool {
|
||||
return $toolbar_item !== '|';
|
||||
})),
|
||||
'defaultItem' => 'drupalElementStyle:align:breakText',
|
||||
],
|
||||
];
|
||||
$plugin_definitions['media_mediaAlign'] = new CKEditor5PluginDefinition($media_align_plugin_definition);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
# This plugin is for testing deprecation of CKEditor 5 icon names before version 45.
|
||||
ckeditor5_icon_deprecation_test_plugin:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
config:
|
||||
drupalElementStyles:
|
||||
align:
|
||||
# This is a valid icon name.
|
||||
- name: 'IconObjectCenter'
|
||||
title: 'Icon object center'
|
||||
icon: IconObjectCenter
|
||||
modelElements: ['drupalMedia']
|
||||
# The next four are deprecated icon names with specifically mapped to v45 icon names.
|
||||
- name: 'objectBlockLeft'
|
||||
title: 'Object block left'
|
||||
icon: objectBlockLeft
|
||||
modelElements: ['drupalMedia']
|
||||
- name: 'objectBlockRight'
|
||||
title: 'Object block right'
|
||||
icon: objectBlockRight
|
||||
modelElements: [ 'drupalMedia' ]
|
||||
- name: 'objectLeft'
|
||||
title: 'Object left'
|
||||
icon: objectLeft
|
||||
modelElements: [ 'drupalMedia' ]
|
||||
- name: 'objectRight'
|
||||
title: 'Object right'
|
||||
icon: objectRight
|
||||
modelElements: [ 'drupalMedia' ]
|
||||
svg:
|
||||
# Icon set as SVG XML.
|
||||
- name: 'svg'
|
||||
title: 'SVG'
|
||||
icon: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path opacity=".5" d="M2 3h16v1.5H2zm0 12h16v1.5H2z"/><path d="M18.003 7v5.5a1 1 0 0 1-1 1H8.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H9.5V12h6.997V7.5z"/></svg>'
|
||||
modelElements: [ 'drupalMedia' ]
|
||||
threeVerticalDots:
|
||||
# This is a deprecated icon name mapped with general rule 'exampleName' -> 'IconExampleName'.
|
||||
- name: 'threeVerticalDots'
|
||||
title: 'Three vertical dots'
|
||||
icon: threeVerticalDots
|
||||
modelElements: [ 'drupalMedia' ]
|
||||
|
||||
drupal:
|
||||
label: Deprecated icons
|
||||
elements:
|
||||
- <drupal-media>
|
||||
@ -0,0 +1,7 @@
|
||||
name: CKEditor icon deprecation test
|
||||
type: module
|
||||
description: "Provides test CKEditor5 plugin with deprecated Drupal element styles icon config"
|
||||
package: Testing
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Incompatible Filter Test
|
||||
type: module
|
||||
description: "Provides a filter incompatible with CKEditor 5"
|
||||
package: Testing
|
||||
dependencies:
|
||||
- drupal:ckeditor5
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor5_incompatible_filter_test\Plugin\Filter;
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\filter\Attribute\Filter;
|
||||
use Drupal\filter\FilterProcessResult;
|
||||
use Drupal\filter\Plugin\FilterBase;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
|
||||
/**
|
||||
* Provides a filter incompatible with CKEditor 5.
|
||||
*/
|
||||
#[Filter(
|
||||
id: "filter_incompatible",
|
||||
title: new TranslatableMarkup("A TYPE_MARKUP_LANGUAGE filter incompatible with CKEditor 5"),
|
||||
type: FilterInterface::TYPE_MARKUP_LANGUAGE
|
||||
)]
|
||||
class FilterIsIncompatible extends FilterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process($text, $langcode) {
|
||||
return new FilterProcessResult($text);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
ckeditor5_manual_decorator_test_openInNewTab:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
config:
|
||||
link:
|
||||
decorators:
|
||||
openInNewTab:
|
||||
mode: 'manual'
|
||||
label: 'Open in a new tab'
|
||||
attributes:
|
||||
target: '_blank'
|
||||
rel: 'noopener noreferrer'
|
||||
classes: ['link-new-tab']
|
||||
pinkColor:
|
||||
mode: 'manual'
|
||||
label: 'Pink color'
|
||||
styles:
|
||||
color: 'pink'
|
||||
drupal:
|
||||
label: Open in new tab
|
||||
elements:
|
||||
- <a target="_blank" rel="noopener noreferrer" class>
|
||||
conditions:
|
||||
plugins:
|
||||
- ckeditor5_link
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Manual Decorator Test
|
||||
type: module
|
||||
description: "Provides configuration for CKEditor 5 link plugin manual decorator."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,26 @@
|
||||
ckeditor5_plugin_conditions_test_plugins_condition:
|
||||
ckeditor5:
|
||||
plugins: {}
|
||||
drupal:
|
||||
label: TEST — Plugins Condition
|
||||
toolbar_items:
|
||||
fooBarConditions:
|
||||
label: Foo Bar (Test Plugins Condition)
|
||||
conditions:
|
||||
plugins:
|
||||
- ckeditor5_heading
|
||||
- ckeditor5_table
|
||||
elements:
|
||||
- <foo>
|
||||
|
||||
ckeditor5_plugin_conditions_test_plugin_allow_all_classes_on_kbd:
|
||||
ckeditor5:
|
||||
plugins: {}
|
||||
drupal:
|
||||
label: TEST — Allow all classes on kbd
|
||||
toolbar_items:
|
||||
kbdAllClasses:
|
||||
label: All classes on kbd (Test Plugins Condition)
|
||||
elements:
|
||||
- <kbd>
|
||||
- <kbd class>
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Plugin Conditions Test
|
||||
type: module
|
||||
description: "Provides test plugins for CKEditor 5 to test plugin conditions."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,11 @@
|
||||
ckeditor5_plugin_elements_subset_sneakySuperset:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: Sneaky Superset
|
||||
class: Drupal\ckeditor5_plugin_elements_subset\Plugin\CKEditor5Plugin\SneakySuperset
|
||||
elements:
|
||||
- <foo>
|
||||
- <bar>
|
||||
- <bar baz>
|
||||
- <$any-html5-element class>
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Plugin Elements Subset Test
|
||||
type: module
|
||||
description: "Provides test plugin to test CKEditor5PluginElementsSubsetInterface"
|
||||
package: Testing
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,10 @@
|
||||
ckeditor5.plugin.ckeditor5_plugin_elements_subset_sneakySuperset:
|
||||
type: mapping
|
||||
label: Sneaky Superset
|
||||
mapping:
|
||||
configured_subset:
|
||||
type: sequence
|
||||
label: 'Allowed Tags'
|
||||
sequence:
|
||||
type: ckeditor5.element
|
||||
label: 'Allowed Tag'
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor5_plugin_elements_subset\Plugin\CKEditor5Plugin;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginElementsSubsetInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Provides a plugin for testing CKEditor.
|
||||
*/
|
||||
class SneakySuperset extends CKEditor5PluginDefault implements CKEditor5PluginElementsSubsetInterface {
|
||||
|
||||
use CKEditor5PluginConfigurableTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defaultConfiguration() {
|
||||
return [
|
||||
'configured_subset' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getElementsSubset(): array {
|
||||
return $this->configuration['configured_subset'];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
# cspell:ignore everytextcontainer justheading
|
||||
ckeditor5_plugin_elements_test_headingCombo:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — block quote combo
|
||||
elements:
|
||||
- <h1 data-justheading>
|
||||
- <$text-container data-everytextcontainer>
|
||||
|
||||
ckeditor5_plugin_elements_test_headingsWithOtherAttributes:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — headings with other attributes
|
||||
elements:
|
||||
- <h1 data-just-h1>
|
||||
- <h2 class="additional-allowed-class">
|
||||
- <h3 data-just-h3 data-just-h3-limited="i-am-the-only-allowed-value">
|
||||
- <h5 data-just-h5-limited="first-allowed-value second-allowed-value">
|
||||
|
||||
ckeditor5_plugin_elements_test_headingsUseClassAnyValue:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
drupal:
|
||||
label: TEST — headings with any class
|
||||
elements:
|
||||
- <h1 class>
|
||||
- <h2 class>
|
||||
- <h3 class>
|
||||
- <h4 class>
|
||||
- <h5 class>
|
||||
- <h6 class>
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Plugin Elements Test
|
||||
type: module
|
||||
description: "Provides test plugins for CKEditor 5 to test allowed elements parsing."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 read-only mode test
|
||||
type: module
|
||||
description: "Provides code for testing disabled CKEditor 5 editors."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor5_read_only_mode\Hook;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for ckeditor5_read_only_mode.
|
||||
*/
|
||||
class Ckeditor5ReadOnlyModeHooks {
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
#[Hook('form_node_page_form_alter')]
|
||||
public function formNodePageFormAlter(array &$form, FormStateInterface $form_state, string $form_id) : void {
|
||||
$form['body']['#disabled'] = \Drupal::state()->get('ckeditor5_read_only_mode_body_enabled', FALSE);
|
||||
$form['field_second_ckeditor5_field']['#disabled'] = \Drupal::state()->get('ckeditor5_read_only_mode_second_ckeditor5_field_enabled', FALSE);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
# cspell:ignore layercake
|
||||
ckeditor5_test_layercake:
|
||||
ckeditor5:
|
||||
plugins: []
|
||||
config:
|
||||
drupalElementStyles:
|
||||
layercake:
|
||||
- name: 'layerCakeSide'
|
||||
title: 'Media aligned to side'
|
||||
icon: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path opacity=".5" d="M2 3h16v1.5H2zm0 12h16v1.5H2z"/><path d="M18.003 7v5.5a1 1 0 0 1-1 1H8.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H9.5V12h6.997V7.5z"/></svg>'
|
||||
attributeName: 'class'
|
||||
attributeValue: 'layercake-side'
|
||||
modelElements: ['drupalMedia']
|
||||
drupalMediaStyles:
|
||||
toolbar:
|
||||
- drupalElementStyle:layerCakeSide
|
||||
drupal:
|
||||
label: TEST — Layercake
|
||||
library: ckeditor5_test/layercake
|
||||
toolbar_items:
|
||||
simpleBox:
|
||||
label: Simple Box
|
||||
twoCol:
|
||||
label: Two Col layout
|
||||
elements:
|
||||
- <h1 class>
|
||||
- <div class>
|
||||
- <section class>
|
||||
- <drupal-media class="layercake-side">
|
||||
@ -0,0 +1,7 @@
|
||||
name: CKEditor 5 Test
|
||||
type: module
|
||||
description: "Provides test layout and component plugins for CKEditor 5."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- drupal:editor
|
||||
- ckeditor5:ckeditor5
|
||||
@ -0,0 +1,10 @@
|
||||
# cspell:ignore layercake
|
||||
layercake:
|
||||
version: VERSION
|
||||
# In real-world (non-test) scenarios, this would load the CKEditor 5 plugin's built JS.
|
||||
js: {}
|
||||
css:
|
||||
theme:
|
||||
css/layout.css: {}
|
||||
dependencies:
|
||||
- core/ckeditor5
|
||||
@ -0,0 +1,13 @@
|
||||
ckeditor5_test.off_canvas:
|
||||
path: '/ckeditor5_test/off_canvas'
|
||||
defaults:
|
||||
_controller: '\Drupal\ckeditor5_test\Controller\CKEditor5OffCanvasTestController::testOffCanvas'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
||||
|
||||
ckeditor5_test.dialog:
|
||||
path: '/ckeditor5_test/dialog'
|
||||
defaults:
|
||||
_controller: '\Drupal\ckeditor5_test\Controller\CKEditor5DialogTestController::testDialog'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
||||
@ -0,0 +1,14 @@
|
||||
.layout--two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.simple-box {
|
||||
padding: 0.5rem;
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.simple-box-title,
|
||||
.simple-box-description {
|
||||
background: #fff;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor5_test\Controller;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Provides controller for testing CKEditor in off-canvas dialogs.
|
||||
*/
|
||||
class CKEditor5DialogTestController {
|
||||
|
||||
/**
|
||||
* Returns a link that can open a node add form in an modal dialog.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*/
|
||||
public function testDialog() {
|
||||
$build['link'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => 'Add Node',
|
||||
'#url' => Url::fromRoute('node.add', ['node_type' => 'page']),
|
||||
'#attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'dialog',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
'modal' => TRUE,
|
||||
'autoResize' => TRUE,
|
||||
]),
|
||||
],
|
||||
];
|
||||
$build['#attached']['library'][] = 'core/drupal.dialog.ajax';
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor5_test\Controller;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Provides controller for testing CKEditor in off-canvas dialogs.
|
||||
*/
|
||||
class CKEditor5OffCanvasTestController {
|
||||
|
||||
/**
|
||||
* Returns a link that can open a node add form in an off-canvas dialog.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*/
|
||||
public function testOffCanvas() {
|
||||
$build['link'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => 'Add Node',
|
||||
'#url' => Url::fromRoute('node.add', ['node_type' => 'page']),
|
||||
'#attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'dialog',
|
||||
'data-dialog-renderer' => 'off_canvas',
|
||||
],
|
||||
];
|
||||
$build['#attached']['library'][] = 'core/drupal.dialog.off_canvas';
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor 5 Module Allowed Image
|
||||
type: module
|
||||
description: Alters the allowed image types.
|
||||
package: Testing
|
||||
dependencies:
|
||||
- drupal:ckeditor5
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor5_test_module_allowed_image\Hook;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for ckeditor5_test_module_allowed_image.
|
||||
*/
|
||||
class Ckeditor5TestModuleAllowedImageHooks {
|
||||
|
||||
/**
|
||||
* Implements hook_ckeditor5_plugin_info_alter().
|
||||
*/
|
||||
#[Hook('ckeditor5_plugin_info_alter')]
|
||||
public function ckeditor5PluginInfoAlter(array &$plugin_definitions) : void {
|
||||
// Add a custom file type to the image upload plugin. Note that 'svg+xml'
|
||||
// below should be an IANA image media type Name, with the "image/" prefix
|
||||
// omitted. In other words: a subtype of type image.
|
||||
// @see https://www.iana.org/assignments/media-types/media-types.xhtml#image
|
||||
// @see https://ckeditor.com/docs/ckeditor5/latest/api/module_image_imageconfig-ImageUploadConfig.html#member-types
|
||||
$image_upload_plugin_definition = $plugin_definitions['ckeditor5_imageUpload']->toArray();
|
||||
$image_upload_plugin_definition['ckeditor5']['config']['image']['upload']['types'][] = 'svg+xml';
|
||||
$plugin_definitions['ckeditor5_imageUpload'] = new CKEditor5PluginDefinition($image_upload_plugin_definition);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
name: CKEditor Test
|
||||
type: module
|
||||
description: "Provides test a test editor that with the ID ckeditor."
|
||||
package: Testing
|
||||
dependencies:
|
||||
- drupal:editor_test
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor_test\Hook;
|
||||
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for ckeditor_test.
|
||||
*/
|
||||
class CkeditorTestHooks {
|
||||
|
||||
/**
|
||||
* Implements hook_editor_info_alter().
|
||||
*/
|
||||
#[Hook('editor_info_alter')]
|
||||
public function editorInfoAlter(array &$editors): void {
|
||||
// Drupal 9 used to have an editor called ckeditor. Copy the Unicorn editor
|
||||
// to it to be able to test upgrading to CKEditor 5.
|
||||
$editors['ckeditor'] = $editors['unicorn'];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Functional;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Drupal\user\Entity\User;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* Test the ckeditor5-stylesheets theme config property.
|
||||
*
|
||||
* @group ckeditor5
|
||||
*/
|
||||
class AddedStylesheetsTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'ckeditor5',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The editor user.
|
||||
*
|
||||
* @var \Drupal\editor\Entity\Editor
|
||||
*/
|
||||
protected Editor $editor;
|
||||
|
||||
/**
|
||||
* The admin user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected User $adminUser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$filtered_html_format = FilterFormat::create([
|
||||
'format' => 'llama',
|
||||
'name' => 'Llama',
|
||||
'filters' => [],
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
]);
|
||||
$filtered_html_format->save();
|
||||
$this->editor = Editor::create([
|
||||
'format' => 'llama',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->editor->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair($this->editor, $filtered_html_format))
|
||||
));
|
||||
// Create node type.
|
||||
$this->drupalCreateContentType([
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
]);
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'create article content',
|
||||
'use text format llama',
|
||||
'administer themes',
|
||||
'view the administration theme',
|
||||
'administer filters',
|
||||
]);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the ckeditor5-stylesheets theme config.
|
||||
*/
|
||||
public function testCkeditorStylesheets(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
/** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
|
||||
$theme_installer = \Drupal::service('theme_installer');
|
||||
$theme_installer->install(['test_ckeditor_stylesheets_relative', 'claro']);
|
||||
$this->config('system.theme')->set('admin', 'claro')->save();
|
||||
|
||||
$this->drupalGet('node/add/article');
|
||||
$assert_session->responseNotContains('test_ckeditor_stylesheets_relative/css/yokotsoko.css');
|
||||
|
||||
// Confirm that the missing ckeditor5-stylesheets configuration can be
|
||||
// bypassed.
|
||||
$this->drupalGet('admin/config/content/formats/manage/llama');
|
||||
$assert_session->pageTextNotContains('ckeditor_stylesheets configured without a corresponding ckeditor5-stylesheets configuration.');
|
||||
|
||||
// Install a theme with ckeditor5-stylesheets configured. Do this manually
|
||||
// to confirm `library_info` cache tags are invalidated.
|
||||
$this->drupalGet('admin/appearance');
|
||||
$this->clickLink('Set Test relative CKEditor stylesheets as default theme');
|
||||
|
||||
// Confirm the stylesheet added via `ckeditor5-stylesheets` is present.
|
||||
$this->drupalGet('node/add/article');
|
||||
$assert_session->responseContains('test_ckeditor_stylesheets_relative/css/yokotsoko.css');
|
||||
|
||||
// Change the default theme to Stark, and confirm the stylesheet added via
|
||||
// `ckeditor5-stylesheets` is no longer present.
|
||||
$this->drupalGet('admin/appearance');
|
||||
$this->clickLink('Set Stark as default theme');
|
||||
$this->drupalGet('node/add/article');
|
||||
$assert_session->responseNotContains('test_ckeditor_stylesheets_relative/css/yokotsoko.css');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Functional;
|
||||
|
||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
|
||||
|
||||
/**
|
||||
* Generic module test for ckeditor5.
|
||||
*
|
||||
* @group ckeditor5
|
||||
*/
|
||||
class GenericTest extends GenericModuleTestBase {}
|
||||
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Functional;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\File\FileExists;
|
||||
|
||||
/**
|
||||
* Test image upload access.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class ImageUploadAccessTest extends ImageUploadTest {
|
||||
|
||||
/**
|
||||
* Test access to the CKEditor 5 image upload controller.
|
||||
*/
|
||||
public function testCkeditor5ImageUploadRoute(): void {
|
||||
$this->createBasicFormat();
|
||||
$url = $this->getUploadUrl();
|
||||
$test_image = file_get_contents(current($this->getTestFiles('image'))->uri);
|
||||
|
||||
// With no text editor, expect a 404.
|
||||
$response = $this->uploadRequest($url, $test_image, 'test.jpg');
|
||||
$this->assertSame(404, $response->getStatusCode());
|
||||
|
||||
$editor = $this->createEditorWithUpload(['status' => FALSE]);
|
||||
|
||||
// Ensure that images cannot be uploaded when image upload is disabled.
|
||||
$response = $this->uploadRequest($url, $test_image, 'test.jpg');
|
||||
$this->assertSame(403, $response->getStatusCode());
|
||||
|
||||
$editor->setImageUploadSettings([
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => NULL,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
])->save();
|
||||
$response = $this->uploadRequest($url, $test_image, 'test.jpg');
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
|
||||
// Ensure lock failures are reported correctly.
|
||||
$d = 'public://inline-images/test.jpg';
|
||||
$f = $this->container->get('file_system')->getDestinationFilename($d, FileExists::Rename);
|
||||
$this->container->get('lock')
|
||||
->acquire('file:ckeditor5:' . Crypt::hashBase64($f));
|
||||
$response = $this->uploadRequest($url, $test_image, 'test.jpg');
|
||||
$this->assertSame(503, $response->getStatusCode());
|
||||
$this->assertStringContainsString('File "public://inline-images/test_0.jpg" is already locked for writing.', (string) $response->getBody());
|
||||
|
||||
// Ensure that users without permissions to the text format cannot upload
|
||||
// images.
|
||||
$this->drupalLogout();
|
||||
$response = $this->uploadRequest($url, $test_image, 'test.jpg');
|
||||
$this->assertSame(403, $response->getStatusCode());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\ckeditor5\Traits\SynchronizeCsrfTokenSeedTrait;
|
||||
use Drupal\Tests\jsonapi\Functional\JsonApiRequestTestTrait;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\user\RoleInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Test image upload.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class ImageUploadTest extends BrowserTestBase {
|
||||
|
||||
use JsonApiRequestTestTrait;
|
||||
use TestFileCreationTrait;
|
||||
use SynchronizeCsrfTokenSeedTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'editor',
|
||||
'filter',
|
||||
'ckeditor5',
|
||||
];
|
||||
|
||||
/**
|
||||
* A user without any particular permissions to be used in testing.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->user = $this->drupalCreateUser();
|
||||
$this->drupalLogin($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with a disallowed extension.
|
||||
*/
|
||||
public function testUploadFileExtension(): void {
|
||||
$this->createBasicFormat();
|
||||
$this->createEditorWithUpload([
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => NULL,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
]);
|
||||
|
||||
$url = $this->getUploadUrl();
|
||||
$image_file = file_get_contents(current($this->getTestFiles('image'))->uri);
|
||||
$non_image_file = file_get_contents(current($this->getTestFiles('php'))->uri);
|
||||
$response = $this->uploadRequest($url, $non_image_file, 'test.php');
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
|
||||
$response = $this->uploadRequest($url, $image_file, 'test.jpg');
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using the file upload route with a file size larger than allowed.
|
||||
*/
|
||||
public function testFileUploadLargerFileSize(): void {
|
||||
$this->createBasicFormat();
|
||||
$this->createEditorWithUpload([
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => 30000,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
]);
|
||||
|
||||
$url = $this->getUploadUrl();
|
||||
$images = $this->getTestFiles('image');
|
||||
$large_image = $this->getTestImageByStat($images, 'size', function ($size) {
|
||||
return $size > 30000;
|
||||
});
|
||||
$small_image = $this->getTestImageByStat($images, 'size', function ($size) {
|
||||
return $size < 30000;
|
||||
});
|
||||
|
||||
$response = $this->uploadRequest($url, file_get_contents($large_image->uri), 'large.jpg');
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
|
||||
$response = $this->uploadRequest($url, file_get_contents($small_image->uri), 'small.jpg');
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that lock is removed after a failed validation.
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/3184974
|
||||
*/
|
||||
public function testLockAfterFailedValidation(): void {
|
||||
$this->createBasicFormat();
|
||||
$this->createEditorWithUpload([
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => 30000,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
]);
|
||||
|
||||
$url = $this->getUploadUrl();
|
||||
$images = $this->getTestFiles('image');
|
||||
$large_image = $this->getTestImageByStat($images, 'size', function ($size) {
|
||||
return $size > 30000;
|
||||
});
|
||||
$small_image = $this->getTestImageByStat($images, 'size', function ($size) {
|
||||
return $size < 30000;
|
||||
});
|
||||
$response = $this->uploadRequest($url, file_get_contents($large_image->uri), 'same.jpg');
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
|
||||
$response = $this->uploadRequest($url, file_get_contents($small_image->uri), 'same.jpg');
|
||||
$this->assertSame(201, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Make upload request to a controller.
|
||||
*
|
||||
* @param \Drupal\Core\Url $url
|
||||
* The URL for the request.
|
||||
* @param string $file_contents
|
||||
* File contents.
|
||||
* @param string $file_name
|
||||
* Name of the file.
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
* The response.
|
||||
*/
|
||||
protected function uploadRequest(Url $url, string $file_contents, string $file_name): ResponseInterface {
|
||||
$request_options[RequestOptions::HEADERS] = [
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
$request_options[RequestOptions::MULTIPART] = [
|
||||
[
|
||||
'name' => 'upload',
|
||||
'filename' => $file_name,
|
||||
'contents' => $file_contents,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->request('POST', $url, $request_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the image upload URL.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The upload image URL for the basic_html format.
|
||||
*/
|
||||
protected function getUploadUrl() {
|
||||
$token = $this->container->get('csrf_token')->get('ckeditor5/upload-image/basic_html');
|
||||
return Url::fromRoute('ckeditor5.upload_image', ['editor' => 'basic_html'], ['query' => ['token' => $token]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a basic_html text format for the editor to reference.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
*/
|
||||
protected function createBasicFormat(): void {
|
||||
$basic_html_format = FilterFormat::create([
|
||||
'format' => 'basic_html',
|
||||
'name' => 'Basic HTML',
|
||||
'weight' => 1,
|
||||
'filters' => [
|
||||
'filter_html_escape' => ['status' => 1],
|
||||
],
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
]);
|
||||
$basic_html_format->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an editor entity with image_upload config.
|
||||
*
|
||||
* @param array $upload_config
|
||||
* The editor image_upload config.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityBase|\Drupal\Core\Entity\EntityInterface
|
||||
* The text editor entity.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
*/
|
||||
protected function createEditorWithUpload(array $upload_config) {
|
||||
$editor = Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'basic_html',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'drupalInsertImage',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_imageResize' => [
|
||||
'allow_resize' => FALSE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => $upload_config,
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
return $editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first image matching $condition.
|
||||
*
|
||||
* @param array $images
|
||||
* Images created with getTestFiles().
|
||||
* @param string $stat
|
||||
* A key in the array returned from stat().
|
||||
* @param callable $condition
|
||||
* A function to compare a value of the image file.
|
||||
*
|
||||
* @return object|bool
|
||||
* Objects with 'uri', 'filename', and 'name' properties.
|
||||
*/
|
||||
protected function getTestImageByStat(array $images, string $stat, callable $condition) {
|
||||
return current(array_filter($images, function ($image) use ($condition, $stat) {
|
||||
$stats = stat($image->uri);
|
||||
return $condition($stats[$stat]);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Functional;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\Core\Entity\Entity\EntityViewMode;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\language\Entity\ContentLanguageSettings;
|
||||
use Drupal\media\Entity\Media;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\ckeditor5\Traits\SynchronizeCsrfTokenSeedTrait;
|
||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Drupal\user\Entity\User;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* Tests the media entity metadata API.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class MediaEntityMetadataApiTest extends BrowserTestBase {
|
||||
|
||||
use TestFileCreationTrait;
|
||||
use MediaTypeCreationTrait;
|
||||
use SynchronizeCsrfTokenSeedTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'system',
|
||||
'user',
|
||||
'filter',
|
||||
'editor',
|
||||
'ckeditor5',
|
||||
'media',
|
||||
];
|
||||
|
||||
/**
|
||||
* The sample image media entity to use for testing.
|
||||
*
|
||||
* @var \Drupal\media\MediaInterface
|
||||
*/
|
||||
protected $mediaImage;
|
||||
|
||||
/**
|
||||
* The sample file media entity to use for testing.
|
||||
*
|
||||
* @var \Drupal\media\MediaInterface
|
||||
*/
|
||||
protected $mediaFile;
|
||||
|
||||
/**
|
||||
* The editor instance to use for testing.
|
||||
*
|
||||
* @var \Drupal\editor\Entity\Editor
|
||||
*/
|
||||
protected $editor;
|
||||
|
||||
/**
|
||||
* The admin user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected User $adminUser;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Component\Uuid\UuidInterface
|
||||
*/
|
||||
protected $uuidService;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->uuidService = $this->container->get('uuid');
|
||||
EntityViewMode::create([
|
||||
'id' => 'media.view_mode_1',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'enabled' => TRUE,
|
||||
'label' => 'View Mode 1',
|
||||
])->save();
|
||||
EntityViewMode::create([
|
||||
'id' => 'media.view_mode_2',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'enabled' => TRUE,
|
||||
'label' => 'View Mode 2',
|
||||
])->save();
|
||||
|
||||
$filtered_html_format = FilterFormat::create([
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'id' => 'filter_html',
|
||||
'status' => TRUE,
|
||||
'weight' => -10,
|
||||
'settings' => [
|
||||
'allowed_html' => "<p> <br> <drupal-media data-entity-type data-entity-uuid data-view-mode alt>",
|
||||
'filter_html_help' => TRUE,
|
||||
'filter_html_nofollow' => TRUE,
|
||||
],
|
||||
],
|
||||
'media_embed' => [
|
||||
'id' => 'media_embed',
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'default_view_mode' => 'view_mode_1',
|
||||
'allowed_view_modes' => [
|
||||
'view_mode_1' => 'view_mode_1',
|
||||
'view_mode_2' => 'view_mode_2',
|
||||
],
|
||||
'allowed_media_types' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
]);
|
||||
$filtered_html_format->save();
|
||||
$this->editor = Editor::create([
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [],
|
||||
],
|
||||
'plugins' => [
|
||||
'media_media' => [
|
||||
'allow_view_mode_override' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->editor->save();
|
||||
$filtered_html_format->setFilterConfig('media_embed', [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'default_view_mode' => 'view_mode_1',
|
||||
'allowed_media_types' => [],
|
||||
'allowed_view_modes' => [
|
||||
'view_mode_1' => 'view_mode_1',
|
||||
'view_mode_2' => 'view_mode_2',
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair($this->editor, $filtered_html_format))
|
||||
));
|
||||
|
||||
// Create a sample media entity to be embedded.
|
||||
$this->createMediaType('image', ['id' => 'image']);
|
||||
File::create([
|
||||
'uri' => $this->getTestFiles('image')[0]->uri,
|
||||
])->save();
|
||||
$this->mediaImage = Media::create([
|
||||
'bundle' => 'image',
|
||||
'name' => 'Screaming hairy armadillo',
|
||||
'field_media_image' => [
|
||||
[
|
||||
'target_id' => 1,
|
||||
'alt' => 'default alt',
|
||||
'title' => 'default title',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->mediaImage->save();
|
||||
|
||||
$this->createMediaType('file', ['id' => 'file']);
|
||||
File::create([
|
||||
'uri' => $this->getTestFiles('text')[0]->uri,
|
||||
])->save();
|
||||
$this->mediaFile = Media::create([
|
||||
'bundle' => 'file',
|
||||
'name' => 'Information about screaming hairy armadillo',
|
||||
'field_media_file' => [
|
||||
[
|
||||
'target_id' => 2,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->mediaFile->save();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'use text format filtered_html',
|
||||
]);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the media entity metadata API.
|
||||
*/
|
||||
public function testApi(): void {
|
||||
$path = '/ckeditor5/filtered_html/media-entity-metadata';
|
||||
$token = $this->container->get('csrf_token')->get(ltrim($path, '/'));
|
||||
$uuid = $this->mediaImage->uuid();
|
||||
|
||||
$this->drupalGet($path, ['query' => ['token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(400);
|
||||
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $uuid, 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSame(json_encode(["type" => "image", 'imageSourceMetadata' => ['alt' => 'default alt']]), $this->getSession()->getPage()->getContent());
|
||||
|
||||
$this->mediaImage->set('field_media_image', [
|
||||
'target_id' => 1,
|
||||
'alt' => '',
|
||||
'title' => 'default title',
|
||||
])->save();
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $uuid, 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSame(json_encode(['type' => 'image', 'imageSourceMetadata' => ['alt' => '']]), $this->getSession()->getPage()->getContent());
|
||||
|
||||
// Test that setting the media image field to not display alt field also
|
||||
// omits it from the API (which will in turn instruct the CKE5 plugin to not
|
||||
// show it).
|
||||
FieldConfig::loadByName('media', 'image', 'field_media_image')
|
||||
->setSetting('alt_field', FALSE)
|
||||
->save();
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $uuid, 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSame(json_encode(['type' => 'image']), $this->getSession()->getPage()->getContent());
|
||||
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $this->mediaFile->uuid(), 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSame(json_encode(['type' => 'file']), $this->getSession()->getPage()->getContent());
|
||||
|
||||
// Ensure that unpublished media returns 403.
|
||||
$this->mediaImage->setUnpublished()->save();
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $uuid, 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
|
||||
// Ensure that valid, but non-existing UUID returns 404.
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $this->uuidService->generate(), 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(404);
|
||||
|
||||
// Ensure that invalid UUID returns 400.
|
||||
$this->drupalGet($path, ['query' => ['uuid' => '🦙', 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(400);
|
||||
|
||||
// Ensure that users that don't have access to the filter format receive
|
||||
// either 404 or 403.
|
||||
$this->drupalLogout();
|
||||
$token = $this->container->get('csrf_token')->get(ltrim($path, '/'));
|
||||
$this->drupalGet($path, ['token' => $token]);
|
||||
$this->assertSession()->statusCodeEquals(400);
|
||||
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $uuid, 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
|
||||
$this->mediaImage->setPublished()->save();
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $uuid, 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the media entity metadata API with translations.
|
||||
*/
|
||||
public function testApiTranslation(): void {
|
||||
$this->container->get('module_installer')->install(['language', 'content_translation']);
|
||||
$this->resetAll();
|
||||
ConfigurableLanguage::createFromLangcode('fi')->save();
|
||||
$this->container->get('config.factory')->getEditable('language.negotiation')
|
||||
->set('url.source', 'path_prefix')
|
||||
->set('url.prefixes.fi', 'fi')
|
||||
->save();
|
||||
$this->rebuildContainer();
|
||||
ContentLanguageSettings::loadByEntityTypeBundle('media', 'image')
|
||||
->setDefaultLangcode('en')
|
||||
->setLanguageAlterable(TRUE)
|
||||
->save();
|
||||
$media_fi = Media::load($this->mediaImage->id())->addTranslation('fi');
|
||||
$media_fi->field_media_image->setValue([
|
||||
[
|
||||
'target_id' => '1',
|
||||
// cSpell:disable-next-line
|
||||
'alt' => 'oletus alt-teksti kuvalle',
|
||||
],
|
||||
]);
|
||||
$media_fi->save();
|
||||
$uuid = $this->mediaImage->uuid();
|
||||
|
||||
$path = '/ckeditor5/filtered_html/media-entity-metadata';
|
||||
$token = $this->container->get('csrf_token')->get(ltrim($path, '/'));
|
||||
|
||||
// Ensure that translation is returned when language is specified.
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $uuid, 'token' => $token], 'language' => $media_fi->language()]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
// cSpell:disable-next-line
|
||||
$this->assertSame(json_encode(['type' => 'image', 'imageSourceMetadata' => ['alt' => 'oletus alt-teksti kuvalle']]), $this->getSession()->getPage()->getContent());
|
||||
|
||||
// Ensure that default translation is returned when no language is
|
||||
// specified.
|
||||
$this->drupalGet($path, ['query' => ['uuid' => $uuid, 'token' => $token]]);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSame(json_encode(['type' => 'image', 'imageSourceMetadata' => ['alt' => 'default alt']]), $this->getSession()->getPage()->getContent());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,368 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
// cspell:ignore sourceediting xmlhttprequest
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5 in the admin UI.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class AdminUiTest extends CKEditor5TestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'media_library',
|
||||
'editor_test',
|
||||
'ckeditor5_incompatible_filter_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* Confirm settings only trigger AJAX when select value is CKEditor 5.
|
||||
*/
|
||||
public function testSettingsOnlyFireAjaxWithCkeditor5(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
$this->addNewTextFormat();
|
||||
$this->addNewTextFormat('unicorn');
|
||||
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
|
||||
// Enable media embed to trigger an AJAX rebuild.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
|
||||
$this->assertNoAjaxRequestTriggered();
|
||||
$page->checkField('filters[media_embed][status]');
|
||||
$assert_session->assertExpectedAjaxRequest(1);
|
||||
|
||||
// Perform the same steps as above with CKEditor, and confirm AJAX callbacks
|
||||
// are not triggered on settings changes.
|
||||
$this->drupalGet('admin/config/content/formats/manage/unicorn');
|
||||
|
||||
// Enable media embed to confirm a format not using CKEditor 5 will not
|
||||
// trigger an AJAX rebuild.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
|
||||
$page->checkField('filters[media_embed][status]');
|
||||
$this->assertNoAjaxRequestTriggered();
|
||||
|
||||
// Confirm that AJAX updates happen when attempting to switch to CKEditor 5,
|
||||
// even if prevented from doing so by validation.
|
||||
$this->drupalGet('admin/config/content/formats/add');
|
||||
$this->assertFalse($assert_session->elementExists('css', '#edit-name-machine-name-suffix')->isVisible());
|
||||
$name_field = $page->findField('name');
|
||||
$name_field->setValue('trigger validator');
|
||||
$this->assertTrue($assert_session->elementExists('css', '#edit-name-machine-name-suffix')->isVisible());
|
||||
|
||||
// Enable a filter that is incompatible with CKEditor 5, so validation is
|
||||
// triggered when attempting to switch.
|
||||
$incompatible_filter_name = 'filters[filter_incompatible][status]';
|
||||
$this->assertTrue($page->hasUncheckedField($incompatible_filter_name));
|
||||
$page->checkField($incompatible_filter_name);
|
||||
$this->assertNoAjaxRequestTriggered();
|
||||
|
||||
$page->selectFieldOption('editor[editor]', 'ckeditor5');
|
||||
$assert_session->assertExpectedAjaxRequest(1);
|
||||
|
||||
$filter_warning = 'CKEditor 5 only works with HTML-based text formats. The "A TYPE_MARKUP_LANGUAGE filter incompatible with CKEditor 5" (filter_incompatible) filter implies this text format is not HTML anymore.';
|
||||
|
||||
// The presence of this validation error message confirms the AJAX callback
|
||||
// was invoked.
|
||||
$assert_session->pageTextContains($filter_warning);
|
||||
|
||||
// Disable the incompatible filter. This should trigger another AJAX rebuild
|
||||
// which will include the removal of the validation error as the issue has
|
||||
// been corrected.
|
||||
$this->assertTrue($page->hasCheckedField($incompatible_filter_name));
|
||||
$page->uncheckField($incompatible_filter_name);
|
||||
$assert_session->assertExpectedAjaxRequest(2);
|
||||
$assert_session->pageTextNotContains($filter_warning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that no (new) AJAX requests were triggered.
|
||||
*
|
||||
* @param int $expected_cumulative_ajax_request_count
|
||||
* The number of expected observed XHR requests since the page was loaded.
|
||||
*/
|
||||
protected function assertNoAjaxRequestTriggered(int $expected_cumulative_ajax_request_count = 0): void {
|
||||
// In case of no requests triggered at all yet.
|
||||
if ($expected_cumulative_ajax_request_count === 0) {
|
||||
$result = $this->getSession()->evaluateScript(<<<JS
|
||||
(function() {
|
||||
return window.drupalCumulativeXhrCount;
|
||||
}())
|
||||
JS);
|
||||
$this->assertSame(0, $result);
|
||||
}
|
||||
else {
|
||||
// In case of the non-first AJAX request, ensure that no AJAX requests are
|
||||
// in progress.
|
||||
try {
|
||||
$this->assertSession()->assertWaitOnAjaxRequest(500);
|
||||
}
|
||||
catch (\RuntimeException $e) {
|
||||
throw new \LogicException(sprintf('This call to %s claims there no AJAX request was triggered, but this is wrong: %s.', __METHOD__, $e->getMessage()));
|
||||
}
|
||||
catch (\LogicException $e) {
|
||||
// This is the intent: ::assertWaitOnAjaxRequest() should detect an
|
||||
// "incorrect" call, because this assertion is asserting *no* AJAX
|
||||
// requests have been triggered.
|
||||
assert(str_contains($e->getMessage(), 'Unnecessary'));
|
||||
|
||||
$result = $this->getSession()->evaluateScript(<<<JS
|
||||
(function() {
|
||||
return window.drupalCumulativeXhrCount;
|
||||
}())
|
||||
JS);
|
||||
$this->assertSame($expected_cumulative_ajax_request_count, $result);
|
||||
}
|
||||
}
|
||||
|
||||
// Now that there definitely is no more AJAX request in progress, count the
|
||||
// number of actual XHR requests, ensure they match.
|
||||
$javascript = <<<JS
|
||||
(function(){
|
||||
return window.performance
|
||||
.getEntries()
|
||||
.filter(entry => entry.initiatorType === 'xmlhttprequest')
|
||||
.length
|
||||
})()
|
||||
JS;
|
||||
$this->assertSame($expected_cumulative_ajax_request_count, $this->getSession()->evaluateScript($javascript));
|
||||
}
|
||||
|
||||
/**
|
||||
* CKEditor 5's filter UI modifications should not break it for other editors.
|
||||
*/
|
||||
public function testUnavailableFiltersHiddenWhenSwitching(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
$this->createNewTextFormat($page, $assert_session, 'unicorn');
|
||||
$assert_session->pageTextNotContains('Filter settings');
|
||||
|
||||
// Switching to CKEditor 5 should keep the filter settings hidden.
|
||||
$page->selectFieldOption('editor[editor]', 'ckeditor5');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$assert_session->pageTextNotContains('Filter settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that filter settings are only visible when the filter is enabled.
|
||||
*/
|
||||
public function testFilterCheckboxesToggleSettings(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
$media_tab = $page->find('css', '[href^="#edit-filters-media-embed-settings"]');
|
||||
$this->assertFalse($media_tab->isVisible(), 'Media filter settings should not be present because media filter is not enabled');
|
||||
|
||||
$this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
|
||||
$page->checkField('filters[media_embed][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
$media_tab = $assert_session->waitForElementVisible('css', '[href^="#edit-filters-media-embed-settings"]');
|
||||
$this->assertTrue($media_tab->isVisible(), 'Media settings should appear when media filter enabled');
|
||||
|
||||
$page->uncheckField('filters[media_embed][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
$media_tab = $page->find('css', '[href^="#edit-filters-media-embed-settings"]');
|
||||
$this->assertFalse($media_tab->isVisible(), 'Media settings should be removed when media filter disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that image upload settings (stored out of band) are validated too.
|
||||
*/
|
||||
public function testImageUploadSettingsAreValidated(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->addNewTextFormat();
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
|
||||
// Add the image plugin to the CKEditor 5 toolbar.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-drupalInsertImage'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage', 'ArrowDown');
|
||||
$assert_session->assertExpectedAjaxRequest(1);
|
||||
|
||||
// Open the vertical tab with its settings.
|
||||
$page->find('css', '[href^="#edit-editor-settings-plugins-ckeditor5-image"]')->click();
|
||||
$this->assertTrue($assert_session->waitForText('Enable image uploads'));
|
||||
|
||||
// Check the "Enable image uploads" checkbox.
|
||||
$assert_session->checkboxNotChecked('editor[settings][plugins][ckeditor5_image][status]');
|
||||
$page->checkField('editor[settings][plugins][ckeditor5_image][status]');
|
||||
$assert_session->assertExpectedAjaxRequest(2);
|
||||
|
||||
// Enter a nonsensical maximum file size.
|
||||
$page->fillField('editor[settings][plugins][ckeditor5_image][max_size]', 'foobar');
|
||||
$this->assertNoRealtimeValidationErrors();
|
||||
|
||||
// Enable another toolbar item to trigger validation.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertExpectedAjaxRequest(3);
|
||||
|
||||
// The expected validation error must be present.
|
||||
$assert_session->elementExists('css', '[role=alert]:contains("This value must be a number of bytes, optionally with a unit such as "MB" or "megabytes".")');
|
||||
|
||||
// Enter no maximum file size because it is optional, this should result in
|
||||
// no validation error and it being set to `null`.
|
||||
$page->findField('editor[settings][plugins][ckeditor5_image][max_size]')->setValue('');
|
||||
|
||||
// Remove a toolbar item to trigger validation.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowUp');
|
||||
$assert_session->assertExpectedAjaxRequest(4);
|
||||
|
||||
// No more validation errors, let's save.
|
||||
$this->assertNoRealtimeValidationErrors();
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextContains('The text format ckeditor5 has been updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure CKEditor 5 admin UI's real-time validation errors do not accumulate.
|
||||
*/
|
||||
public function testMessagesDoNotAccumulate(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->addNewTextFormat();
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
|
||||
// Add the source editing plugin to the CKEditor 5 toolbar.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-sourceEditing'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
$find_validation_error_messages = function () use ($page): array {
|
||||
return $page->findAll('css', '[role=alert]:contains("The following tag(s) are already supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: Bold (<strong>).")');
|
||||
};
|
||||
|
||||
// No validation errors when we start.
|
||||
$this->assertCount(0, $find_validation_error_messages());
|
||||
|
||||
// Configure Source Editing to allow editing `<strong>` to trigger
|
||||
// validation error.
|
||||
$assert_session->waitForText('Source editing');
|
||||
$page->find('css', '[href^="#edit-editor-settings-plugins-ckeditor5-sourceediting"]')->click();
|
||||
$assert_session->waitForText('Manually editable HTML tags');
|
||||
$source_edit_tags_field = $assert_session->fieldExists('editor[settings][plugins][ckeditor5_sourceEditing][allowed_tags]');
|
||||
$source_edit_tags_field->setValue('<strong>');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertCount(1, $find_validation_error_messages());
|
||||
|
||||
// Revert Source Editing it: validation messages should be gone.
|
||||
$source_edit_tags_field->setValue('');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertCount(0, $find_validation_error_messages());
|
||||
|
||||
// Add `<strong>` again: validation messages should be back.
|
||||
$source_edit_tags_field->setValue('<strong>');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertCount(1, $find_validation_error_messages());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the plugin settings form section.
|
||||
*/
|
||||
public function testPluginSettingsFormSection(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
// The default toolbar only enables the configurable heading plugin and the
|
||||
// non-configurable bold and italic plugins.
|
||||
$assert_session->fieldValueEquals('editor[settings][toolbar][items]', '["heading","bold","italic"]');
|
||||
// The heading plugin config form should be present.
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-heading"]');
|
||||
|
||||
// Remove the heading plugin from the toolbar.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-heading', 'ArrowUp');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The heading plugin config form should no longer be present.
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-heading"]');
|
||||
// The plugin settings wrapper should still be present, but empty.
|
||||
$assert_session->elementExists('css', '#plugin-settings-wrapper');
|
||||
$assert_session->elementNotContains('css', '#plugin-settings-wrapper', '<div');
|
||||
|
||||
// Enable the source plugin.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The source plugin config form should be present.
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting"]');
|
||||
|
||||
// The filter-dependent configurable plugin should not be present.
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-media-media"]');
|
||||
|
||||
// Enable the filter that the configurable plugin depends on.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
|
||||
$page->checkField('filters[media_embed][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The filter-dependent configurable plugin should be present.
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-media-media"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the language config form.
|
||||
*/
|
||||
public function testLanguageConfigForm(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
// The language plugin config form should not be present.
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-language"]');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-textPartLanguage'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-textPartLanguage', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The CKEditor 5 module should warn that `<span>` cannot be created.
|
||||
$assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="warning"]:contains("The Language plugin needs another plugin to create <span>, for it to be able to create the following attributes: <span lang dir>. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.")');
|
||||
|
||||
// Make `<span>` creatable.
|
||||
$this->assertNotEmpty($assert_session->elementExists('css', '.ckeditor5-toolbar-item-sourceEditing'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
// The Source Editing plugin settings form should now be present and should
|
||||
// have no allowed tags configured.
|
||||
$page->clickLink('Source editing');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]'));
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]');
|
||||
allowedTags.value = '<span>';
|
||||
allowedTags.dispatchEvent(new Event('input'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
// Dispatching an `input` event does not work in WebDriver. Enabling another
|
||||
// toolbar item which has no associated HTML elements forces it.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-undo', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Confirm there are no longer any warnings.
|
||||
$assert_session->waitForElementRemoved('css', '[data-drupal-messages] [role="alert"]');
|
||||
|
||||
// The language plugin config form should now be present.
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-language"]');
|
||||
|
||||
// It must also be possible to remove the language plugin again.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-textPartLanguage', 'ArrowUp');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The language plugin config form should not be present anymore.
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-language"]');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,502 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityViewMode;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
// cspell:ignore esque imageUpload sourceediting Editing's
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5AllowedTagsTest extends CKEditor5TestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'editor_test',
|
||||
'ckeditor5',
|
||||
'media',
|
||||
'media_library',
|
||||
'ckeditor5_incompatible_filter_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* The default CKEditor 5 allowed elements.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $allowedElements = '<br> <p> <h2> <h3> <h4> <h5> <h6> <strong> <em>';
|
||||
|
||||
/**
|
||||
* The default allowed elements for filter_html's "allowed_html" setting.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see \Drupal\filter\Plugin\Filter\FilterHtml
|
||||
*/
|
||||
protected $defaultElementsWhenUpdatingNotCkeditor5 = "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id>";
|
||||
|
||||
/**
|
||||
* The expected allowed elements after updating to CKEditor 5.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultElementsAfterUpdatingToCkeditor5 = '<br> <p> <h2 id="jump-*"> <h3 id> <h4 id> <h5 id> <h6 id> <cite> <dl> <dt> <dd> <a hreflang href> <blockquote cite> <ul type> <ol type="1 A I" reversed start> <strong> <em> <code> <li>';
|
||||
|
||||
/**
|
||||
* Test enabling CKEditor 5 in a way that triggers validation.
|
||||
*/
|
||||
public function testEnablingToVersion5Validation(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$incompatible_filter_name = 'filters[filter_incompatible][status]';
|
||||
$filter_warning = 'CKEditor 5 only works with HTML-based text formats. The "A TYPE_MARKUP_LANGUAGE filter incompatible with CKEditor 5" (filter_incompatible) filter implies this text format is not HTML anymore.';
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session, 'unicorn');
|
||||
$page->checkField('filters[filter_html][status]');
|
||||
$page->checkField($incompatible_filter_name);
|
||||
$page->selectFieldOption('editor[editor]', 'ckeditor5');
|
||||
$assert_session->assertExpectedAjaxRequest(2);
|
||||
$assert_session->pageTextContains($filter_warning);
|
||||
|
||||
// Disable the incompatible filter.
|
||||
$page->uncheckField($incompatible_filter_name);
|
||||
|
||||
// Confirm there are no longer any warnings.
|
||||
$assert_session->waitForElementRemoved('css', '[data-drupal-messages] [role="alert"]');
|
||||
|
||||
// Confirm the text format can be saved.
|
||||
$this->saveNewTextFormat($page, $assert_session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that when image uploads were enabled, they remain enabled.
|
||||
*/
|
||||
public function testImageUploadsRemainEnabled(): void {
|
||||
FilterFormat::create([
|
||||
'format' => 'editor_with_image_uploads',
|
||||
'name' => 'Text Editor with image uploads enabled',
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'editor_with_image_uploads',
|
||||
'editor' => 'unicorn',
|
||||
'image_upload' => [
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => NULL,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Assert that image uploads are enabled initially.
|
||||
$this->drupalGet('admin/config/content/formats/manage/editor_with_image_uploads');
|
||||
$this->assertTrue($page->hasCheckedField('Enable image uploads'));
|
||||
|
||||
// Switch the text format to CKEditor 5.
|
||||
$page->selectFieldOption('editor[editor]', 'ckeditor5');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Enable the image toolbar item. This does NOT enable image uploads: it
|
||||
// triggers the image upload settings form to become visible, to allow the
|
||||
// image upload status to be checked.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Assert that image uploads are still enabled.
|
||||
$this->assertTrue($page->hasCheckedField('Enable image uploads'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm that switching to CKEditor 5 from another editor updates tags.
|
||||
*/
|
||||
public function testSwitchToVersion5(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session, 'unicorn');
|
||||
|
||||
// Enable the HTML filter.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[filter_html][status]'));
|
||||
$page->checkField('filters[filter_html][status]');
|
||||
|
||||
// Confirm the allowed HTML tags are the defaults initially.
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $this->defaultElementsWhenUpdatingNotCkeditor5);
|
||||
|
||||
$this->saveNewTextFormat($page, $assert_session);
|
||||
$assert_session->pageTextContains('Added text format unicorn');
|
||||
|
||||
// Return to the config form to confirm that switching text editors on
|
||||
// existing formats will properly switch allowed tags.
|
||||
$this->drupalGet('admin/config/content/formats/manage/unicorn');
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $this->defaultElementsWhenUpdatingNotCkeditor5);
|
||||
|
||||
$page->selectFieldOption('editor[editor]', 'ckeditor5');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$assert_session->pageTextContains('The <br>, <p> tags were added because they are required by CKEditor 5');
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $this->defaultElementsAfterUpdatingToCkeditor5);
|
||||
|
||||
$page->pressButton('Save configuration');
|
||||
|
||||
$assert_session->pageTextContains('The text format unicorn has been updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the img tag is added after enabling image uploads.
|
||||
*/
|
||||
public function testImgAddedViaUploadPlugin(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
$allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$this->assertTrue($allowed_html_field->hasAttribute('readonly'));
|
||||
|
||||
// Allowed tags are currently the default, with no <img>.
|
||||
$this->assertEquals($this->allowedElements, $allowed_html_field->getValue());
|
||||
|
||||
// The image upload settings form should not be present.
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageupload"]');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-drupalInsertImage'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The image upload settings form should now be present.
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-image"]');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-active .ckeditor5-toolbar-item-drupalInsertImage'));
|
||||
|
||||
// The image insert plugin is enabled and inserting <img> is allowed.
|
||||
$this->assertEquals($this->allowedElements . ' <img src alt height width>', $allowed_html_field->getValue());
|
||||
|
||||
$page->clickLink('Image');
|
||||
$assert_session->waitForText('Enable image uploads');
|
||||
$this->assertTrue($page->hasUncheckedField('editor[settings][plugins][ckeditor5_image][status]'));
|
||||
$page->checkField('editor[settings][plugins][ckeditor5_image][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Enabling image uploads adds <img> with several attributes to allowed
|
||||
// tags.
|
||||
$this->assertEquals($this->allowedElements . ' <img src alt height width data-entity-uuid data-entity-type>', $allowed_html_field->getValue());
|
||||
|
||||
// Also enabling the caption filter will add the data-caption attribute to
|
||||
// <img>.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[filter_caption][status]'));
|
||||
$page->checkField('filters[filter_caption][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertEquals($this->allowedElements . ' <img src alt height width data-entity-uuid data-entity-type data-caption>', $allowed_html_field->getValue());
|
||||
|
||||
// Also enabling the alignment filter will add the data-align attribute to
|
||||
// <img>.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[filter_align][status]'));
|
||||
$page->checkField('filters[filter_align][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertEquals($this->allowedElements . ' <img src alt height width data-entity-uuid data-entity-type data-caption data-align>', $allowed_html_field->getValue());
|
||||
|
||||
// Disable image upload.
|
||||
$page->clickLink('Image');
|
||||
$assert_session->waitForText('Enable image uploads');
|
||||
$this->assertTrue($page->hasCheckedField('editor[settings][plugins][ckeditor5_image][status]'));
|
||||
$page->uncheckField('editor[settings][plugins][ckeditor5_image][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The image insert is still allowed when image uploads are disabled.
|
||||
$this->assertEquals($this->allowedElements . ' <img src alt height width data-caption data-align>', $allowed_html_field->getValue());
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-drupalInsertImage'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage', 'ArrowUp');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Confirm <img> is no longer an allowed tag, once image insert is disabled.
|
||||
$this->assertEquals($this->allowedElements, $allowed_html_field->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test filter_html allowed tags.
|
||||
*/
|
||||
public function testAllowedTags(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
// Confirm the "allowed tags" field is read only, and the value
|
||||
// matches the tags required by CKEditor.
|
||||
// Allowed HTML field is readonly and its wrapper has a form-disabled class.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.js-form-item-filters-filter-html-settings-allowed-html.form-disabled'));
|
||||
$allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$this->assertTrue($allowed_html_field->hasAttribute('readonly'));
|
||||
$this->assertSame($this->allowedElements, $allowed_html_field->getValue());
|
||||
$this->saveNewTextFormat($page, $assert_session);
|
||||
|
||||
$assert_session->pageTextContains('Added text format ckeditor5');
|
||||
$assert_session->pageTextContains('Text formats and editors');
|
||||
|
||||
// Confirm the filter config was updated with the correct allowed tags.
|
||||
$this->assertSame($this->allowedElements, FilterFormat::load('ckeditor5')->filters('filter_html')->getConfiguration()['settings']['allowed_html']);
|
||||
|
||||
$page->find('css', '[data-drupal-selector="edit-formats-ckeditor5"]')->clickLink('Configure');
|
||||
|
||||
// Add the block quote plugin to the CKEditor 5 toolbar.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-blockQuote'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-blockQuote', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
$allowed_with_blockquote = $this->allowedElements . ' <blockquote>';
|
||||
$assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $allowed_with_blockquote);
|
||||
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextContains('The text format ckeditor5 has been updated.');
|
||||
|
||||
// Flush caches so the updated config can be checked.
|
||||
drupal_flush_all_caches();
|
||||
|
||||
// Confirm that the tags required by the newly-added plugins were correctly
|
||||
// saved.
|
||||
$this->assertSame($allowed_with_blockquote, FilterFormat::load('ckeditor5')->filters('filter_html')->getConfiguration()['settings']['allowed_html']);
|
||||
|
||||
$page->find('css', '[data-drupal-selector="edit-formats-ckeditor5"]')->clickLink('Configure');
|
||||
|
||||
// And for good measure, confirm the correct tags are in the form field when
|
||||
// returning to the form.
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $allowed_with_blockquote);
|
||||
|
||||
// Add the source editing plugin to the CKEditor 5 toolbar.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-available .ckeditor5-toolbar-item-sourceEditing'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Updating Source Editing's editable tags should automatically update
|
||||
// filter_html to include those additional tags.
|
||||
$assert_session->waitForText('Source editing');
|
||||
$page->find('css', '[href^="#edit-editor-settings-plugins-ckeditor5-sourceediting"]')->click();
|
||||
$assert_session->waitForText('Manually editable HTML tags');
|
||||
$source_edit_tags_field = $assert_session->fieldExists('editor[settings][plugins][ckeditor5_sourceEditing][allowed_tags]');
|
||||
$source_edit_tags_field->setValue('<aside>');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', '<br> <p> <h2> <h3> <h4> <h5> <h6> <aside> <strong> <em> <blockquote>');
|
||||
$allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$this->assertTrue($allowed_html_field->hasAttribute('readonly'));
|
||||
|
||||
// Adding tags to Source Editing's editable tags that are already supported
|
||||
// by enabled CKEditor 5 plugins must trigger a validation error, and that
|
||||
// error must be associated with the correct form item.
|
||||
$source_edit_tags_field->setValue('<aside><strong>');
|
||||
$assert_session->waitForText('The following tag(s) are already supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: Bold (<strong>)');
|
||||
$this->assertTrue($page->find('css', '[href^="#edit-editor-settings-plugins-ckeditor5-sourceediting"]')->getParent()->hasClass('is-selected'));
|
||||
$this->assertSame('true', $page->findField('editor[settings][plugins][ckeditor5_sourceEditing][allowed_tags]')->getAttribute('aria-invalid'));
|
||||
$this->assertTrue($allowed_html_field->hasAttribute('readonly'));
|
||||
|
||||
// The same validation error appears when saving the form regardless of the
|
||||
// immediate AJAX validation error above.
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextContains('The following tag(s) are already supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: Bold (<strong>)');
|
||||
$this->assertTrue($page->find('css', '[href^="#edit-editor-settings-plugins-ckeditor5-sourceediting"]')->getParent()->hasClass('is-selected'));
|
||||
$this->assertSame('true', $page->findField('editor[settings][plugins][ckeditor5_sourceEditing][allowed_tags]')->getAttribute('aria-invalid'));
|
||||
$assert_session->pageTextNotContains('The text format ckeditor5 has been updated');
|
||||
|
||||
// Wait for the "Source editing" vertical tab to appear, remove the already
|
||||
// supported tags and re-save. Now the text format should save successfully.
|
||||
$assert_session->waitForText('Source editing');
|
||||
$page->find('css', '[href^="#edit-editor-settings-plugins-ckeditor5-sourceediting"]')->click();
|
||||
$assert_session->pageTextContains('Manually editable HTML tags');
|
||||
$source_edit_tags_field = $assert_session->fieldExists('editor[settings][plugins][ckeditor5_sourceEditing][allowed_tags]');
|
||||
$source_edit_tags_field->setValue('<aside>');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextContains('The text format ckeditor5 has been updated');
|
||||
$assert_session->pageTextNotContains('The following tag(s) are already supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: Bold (<strong>)');
|
||||
|
||||
// Ensure that CKEditor can be initialized with Source Editing.
|
||||
// @see https://www.drupal.org/i/3231427
|
||||
$this->drupalGet('node/add');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that <drupal-media> is added to allowed tags when media embed enabled.
|
||||
*/
|
||||
public function testMediaElementAllowedTags(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
EntityViewMode::create([
|
||||
'id' => 'media.view_mode_1',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'enabled' => TRUE,
|
||||
'label' => 'View Mode 1',
|
||||
])->save();
|
||||
EntityViewMode::create([
|
||||
'id' => 'media.view_mode_2',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'enabled' => TRUE,
|
||||
'label' => 'View Mode 2',
|
||||
])->save();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
// Allowed HTML field is readonly and its wrapper has a form-disabled class.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.js-form-item-filters-filter-html-settings-allowed-html.form-disabled'));
|
||||
$allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$this->assertTrue($allowed_html_field->hasAttribute('readonly'));
|
||||
|
||||
// Allowed tags are currently the default, with no <drupal-media>.
|
||||
$this->assertEquals($this->allowedElements, $allowed_html_field->getValue());
|
||||
|
||||
// Enable media embed.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
|
||||
$this->assertNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0));
|
||||
$page->checkField('filters[media_embed][status]');
|
||||
$assert_session->assertExpectedAjaxRequest(2);
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0));
|
||||
|
||||
$page->clickLink('Embed media');
|
||||
$page->checkField('filters[media_embed][settings][allowed_view_modes][view_mode_1]');
|
||||
$page->checkField('filters[media_embed][settings][allowed_view_modes][view_mode_2]');
|
||||
|
||||
$allowed_with_media = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt data-view-mode>';
|
||||
$allowed_with_media_without_view_mode = $this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt>';
|
||||
$page->clickLink('Media');
|
||||
$this->assertTrue($page->hasUncheckedField('editor[settings][plugins][media_media][allow_view_mode_override]'));
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $allowed_with_media_without_view_mode);
|
||||
$page->checkField('editor[settings][plugins][media_media][allow_view_mode_override]');
|
||||
$assert_session->assertExpectedAjaxRequest(3);
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $allowed_with_media);
|
||||
$this->saveNewTextFormat($page, $assert_session);
|
||||
$assert_session->pageTextContains('Added text format ckeditor5.');
|
||||
|
||||
// Confirm <drupal-media> was added to allowed tags on save, as a result of
|
||||
// enabling the media embed filter.
|
||||
$this->assertSame($allowed_with_media, FilterFormat::load('ckeditor5')->filters('filter_html')->getConfiguration()['settings']['allowed_html']);
|
||||
|
||||
$page->find('css', '[data-drupal-selector="edit-formats-ckeditor5"]')->clickLink('Configure');
|
||||
|
||||
// Confirm that <drupal-media> is now included in the "Allowed tags" form
|
||||
// field.
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $allowed_with_media);
|
||||
|
||||
// Ensure that data-align attribute is added to <drupal-media> when
|
||||
// filter_align is enabled.
|
||||
$page->checkField('filters[filter_align][status]');
|
||||
$assert_session->assertExpectedAjaxRequest(1);
|
||||
$this->assertEquals($this->allowedElements . ' <drupal-media data-entity-type data-entity-uuid alt data-view-mode data-align>', $allowed_html_field->getValue());
|
||||
|
||||
// Disable media embed.
|
||||
$this->assertTrue($page->hasCheckedField('filters[media_embed][status]'));
|
||||
$page->uncheckField('filters[media_embed][status]');
|
||||
|
||||
$assert_session->assertExpectedAjaxRequest(2);
|
||||
// Confirm allowed tags no longer has <drupal-media>.
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', $this->allowedElements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests full HTML text format.
|
||||
*/
|
||||
public function testFullHtml(): void {
|
||||
FilterFormat::create(
|
||||
Yaml::parseFile('core/profiles/standard/config/install/filter.format.full_html.yml')
|
||||
)->save();
|
||||
FilterFormat::create(
|
||||
Yaml::parseFile('core/profiles/standard/config/install/filter.format.basic_html.yml')
|
||||
)->save();
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Add a node with text rendered via the Plain Text format.
|
||||
$this->drupalGet('node/add');
|
||||
$page->fillField('title[0][value]', 'My test content');
|
||||
$page->fillField('body[0][value]', '<foo bar="baz">⬅️✌️➡️</foo><p><a style="color:#ff0000;" foo="bar" hreflang="en" href="https://example.com"><abbr title="National Aeronautics and Space Administration">NASA</abbr> is an acronym.</a></p>');
|
||||
$page->pressButton('Save');
|
||||
|
||||
// Configure Full HTML text format to use CKEditor 5.
|
||||
$this->drupalGet('admin/config/content/formats/manage/full_html');
|
||||
$page->checkField('roles[authenticated]');
|
||||
$page->selectFieldOption('editor[editor]', 'ckeditor5');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$page->pressButton('Save configuration');
|
||||
$this->assertTrue($assert_session->waitForText('The text format Full HTML has been updated.'));
|
||||
|
||||
// Change the node's text format to Full HTML.
|
||||
$this->drupalGet('node/1/edit');
|
||||
$filter_tips = $page->find('css', '[data-drupal-format-id="basic_html"]');
|
||||
$this->assertTrue($filter_tips->isVisible());
|
||||
$page->selectFieldOption('body[0][format]', 'full_html');
|
||||
$this->assertNotEmpty($assert_session->waitForText('Change text format?'));
|
||||
// Check the visibility of "Filter tips" by clicking the "Cancel" button.
|
||||
$page->pressButton('Cancel');
|
||||
$this->assertTrue($filter_tips->isVisible());
|
||||
$page->selectFieldOption('body[0][format]', 'full_html');
|
||||
$this->assertNotEmpty($assert_session->waitForText('Change text format?'));
|
||||
$page->pressButton('Continue');
|
||||
|
||||
// Ensure the editor is loaded and ensure that arbitrary markup is retained.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
$page->pressButton('Save');
|
||||
|
||||
// But note that the `style` attribute was stripped by
|
||||
// \Drupal\editor\EditorXssFilter\Standard.
|
||||
$assert_session->responseContains('<foo bar="baz">⬅️✌️➡️</foo><p><a foo="bar" hreflang="en" href="https://example.com"><abbr title="National Aeronautics and Space Administration">NASA</abbr> is an acronym.</a></p>');
|
||||
|
||||
// Ensure attributes are retained after enabling link plugin.
|
||||
$this->drupalGet('admin/config/content/formats/manage/full_html');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-link'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-link', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$page->pressButton('Save configuration');
|
||||
|
||||
$this->drupalGet('node/1/edit');
|
||||
$page->pressButton('Save');
|
||||
|
||||
$assert_session->responseContains('<p><a foo="bar" hreflang="en" href="https://example.com"><abbr title="National Aeronautics and Space Administration">NASA</abbr> is an acronym.</a></p>');
|
||||
|
||||
// Configure Basic HTML text format to use CKE5 and enable the link plugin.
|
||||
$this->drupalGet('admin/config/content/formats/manage/basic_html');
|
||||
$page->checkField('roles[authenticated]');
|
||||
$page->selectFieldOption('editor[editor]', 'ckeditor5');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-available .ckeditor5-toolbar-item-underline'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-underline', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$page->pressButton('Save configuration');
|
||||
$this->assertTrue($assert_session->waitForText('The text format Basic HTML has been updated.'));
|
||||
|
||||
// Change the node's text format to Basic HTML.
|
||||
$this->drupalGet('node/1/edit');
|
||||
$page->selectFieldOption('body[0][format]', 'basic_html');
|
||||
$this->assertNotEmpty($assert_session->waitForText('Change text format?'));
|
||||
$page->pressButton('Continue');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$page->pressButton('Save');
|
||||
|
||||
// The `style` and foo` attributes should have been removed, as should the
|
||||
// `<abbr>` and `<foo>` tags.
|
||||
$assert_session->responseContains('<p>⬅️✌️➡️</p><p><a href="https://example.com" hreflang="en">NASA is an acronym.</a></p>');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* Tests code block configured languages are respected.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5CodeSyntaxTest extends CKEditor5TestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* Tests code block configured languages are respected.
|
||||
*/
|
||||
public function testCKEditor5CodeSyntax(): void {
|
||||
$this->addNewTextFormat();
|
||||
/** @var \Drupal\editor\Entity\Editor $editor */
|
||||
$editor = Editor::load('ckeditor5');
|
||||
$editor->setSettings([
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'codeBlock',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_codeBlock' => [
|
||||
'languages' => [
|
||||
['label' => 'Twig', 'language' => 'twig'],
|
||||
['label' => 'YML', 'language' => 'yml'],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->drupalGet('/node/add/page');
|
||||
|
||||
$this->waitForEditor();
|
||||
// Open code block dropdown, and verify that correct languages are present.
|
||||
$assertSession = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->find('css', '.ck-code-block-dropdown .ck-dropdown__button .ck-splitbutton__arrow')->click();
|
||||
$codeBlockOptionsSelector = '.ck-code-block-dropdown .ck-dropdown__panel .ck-list__item .ck-button__label';
|
||||
$assertSession->waitForElementVisible('css', $codeBlockOptionsSelector);
|
||||
$codeBlockOptions = $page->findAll('css', $codeBlockOptionsSelector);
|
||||
$this->assertCount(2, $codeBlockOptions);
|
||||
$this->assertEquals([
|
||||
'Twig',
|
||||
'YML',
|
||||
], \array_map(static fn (NodeElement $el) => $el->getText(), $codeBlockOptions));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5 to ensure correct focus management in dialogs.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5DialogTest extends CKEditor5TestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'ckeditor5',
|
||||
'ckeditor5_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests if CKEditor 5 tooltips can be interacted with in dialogs.
|
||||
*/
|
||||
public function testCKEditor5FocusInTooltipsInDialog(): void {
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'CKEditor 5 with link',
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'test_format',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => ['link'],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->drupalGet('/ckeditor5_test/dialog');
|
||||
$page->clickLink('Add Node');
|
||||
$assert_session->waitForElementVisible('css', '[role="dialog"]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
$content_area = $assert_session->waitForElementVisible('css', '.ck-editor__editable');
|
||||
// Focus the editable area first.
|
||||
$content_area->click();
|
||||
// Then press the button to add a link.
|
||||
$this->pressEditorButton('Link');
|
||||
|
||||
$link_url = '/ckeditor5_test/dialog';
|
||||
$input = $assert_session->waitForElementVisible('css', '.ck-balloon-panel input.ck-input-text');
|
||||
// Make sure the input field can have focus and we can type into it.
|
||||
$input->setValue($link_url);
|
||||
// Save the new link.
|
||||
$page->find('xpath', "//button[span[text()='Insert']]")->click();
|
||||
// Make sure something was added to the text.
|
||||
$this->assertNotEmpty($content_area->getText());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityFormDisplay;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\user\RoleInterface;
|
||||
|
||||
/**
|
||||
* Tests that the fragment link points to CKEditor 5.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5FragmentLinkTest extends WebDriverTestBase {
|
||||
|
||||
use TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['node', 'ckeditor5'];
|
||||
|
||||
/**
|
||||
* The admin user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected User $account;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Create a text format and associate CKEditor 5.
|
||||
FilterFormat::create([
|
||||
'format' => 'ckeditor5',
|
||||
'name' => 'CKEditor 5 with image upload',
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'ckeditor5',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
|
||||
// Create a node type for testing.
|
||||
NodeType::create(['type' => 'page', 'name' => 'page'])->save();
|
||||
|
||||
$field_storage = FieldStorageConfig::loadByName('node', 'body');
|
||||
|
||||
// Create a body field instance for the 'page' node type.
|
||||
FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'page',
|
||||
'label' => 'Body',
|
||||
'settings' => ['display_summary' => TRUE],
|
||||
'required' => TRUE,
|
||||
])->save();
|
||||
|
||||
// Assign widget settings for the 'default' form mode.
|
||||
EntityFormDisplay::create([
|
||||
'targetEntityType' => 'node',
|
||||
'bundle' => 'page',
|
||||
'mode' => 'default',
|
||||
'status' => TRUE,
|
||||
])->setComponent('body', ['type' => 'text_textarea_with_summary'])
|
||||
->save();
|
||||
|
||||
$this->account = $this->drupalCreateUser([
|
||||
'administer nodes',
|
||||
'create page content',
|
||||
]);
|
||||
$this->drupalLogin($this->account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the fragment link to a textarea works with CKEditor 5 enabled.
|
||||
*/
|
||||
public function testFragmentLink(): void {
|
||||
$session = $this->getSession();
|
||||
$web_assert = $this->assertSession();
|
||||
$ckeditor_class = '.ck-editor';
|
||||
$ckeditor_id = '#cke_edit-body-0-value';
|
||||
|
||||
$this->drupalGet('node/add/page');
|
||||
|
||||
// Add a bottom margin to the title field to be sure the body field is not
|
||||
// visible.
|
||||
$session->executeScript("document.getElementById('edit-title-0-value').style.marginBottom = window.innerHeight*2 +'px';");
|
||||
|
||||
$this->assertSession()->waitForElementVisible('css', $ckeditor_id);
|
||||
// Check that the CKEditor 5-enabled body field is currently not visible in
|
||||
// the viewport.
|
||||
$web_assert->assertNotVisibleInViewport('css', $ckeditor_class, 'topLeft', 'CKEditor 5-enabled body field is visible.');
|
||||
|
||||
$before_url = $session->getCurrentUrl();
|
||||
|
||||
// Trigger a hash change with as target the hidden textarea.
|
||||
$session->executeScript("location.hash = '#edit-body-0-value';");
|
||||
|
||||
// Check that the CKEditor 5-enabled body field is visible in the viewport.
|
||||
// The hash change adds an ID to the CKEditor 5 instance so check its
|
||||
// visibility using the ID now.
|
||||
$web_assert->assertVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor 5-enabled body field is not visible.');
|
||||
|
||||
// Use JavaScript to go back in the history instead of
|
||||
// \Behat\Mink\Session::back() because that function doesn't work after a
|
||||
// hash change.
|
||||
$session->executeScript("history.back();");
|
||||
|
||||
$after_url = $session->getCurrentUrl();
|
||||
|
||||
// Check that going back in the history worked.
|
||||
self::assertEquals($before_url, $after_url, 'History back works.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* Tests ckeditor height respects field rows config.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5HeightTest extends CKEditor5TestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* Tests editor height respects rows config.
|
||||
*/
|
||||
public function testCKEditor5Height(): void {
|
||||
$this->addNewTextFormat();
|
||||
/** @var \Drupal\editor\Entity\Editor $editor */
|
||||
$editor = Editor::load('ckeditor5');
|
||||
$editor->setSettings([
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'sourceEditing',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->drupalGet('/node/add/page');
|
||||
$this->waitForEditor();
|
||||
|
||||
// We expect height to be 320, but test to ensure that it's greater
|
||||
// than 300. We want to ensure that we don't hard code a very specific
|
||||
// value because tests might break if styles change (line-height, etc).
|
||||
// Note that the default height for CKEditor5 is 47px.
|
||||
$this->assertGreaterThan(300, $this->getEditorHeight());
|
||||
// Check source editing height.
|
||||
$this->pressEditorButton('Source');
|
||||
$assert = $this->assertSession();
|
||||
$this->assertNotNull($assert->waitForElementVisible('css', '.ck-source-editing-area'));
|
||||
$this->assertGreaterThan(300, $this->getEditorHeight(TRUE));
|
||||
|
||||
// Test the max height of the editor is less that the window height.
|
||||
$body = \str_repeat('<p>Llamas are cute.</p>', 100);
|
||||
$node = $this->drupalCreateNode([
|
||||
'body' => $body,
|
||||
]);
|
||||
$this->drupalGet($node->toUrl('edit-form'));
|
||||
$this->assertLessThan($this->getWindowHeight(), $this->getEditorHeight());
|
||||
|
||||
// Check source editing has a scroll bar.
|
||||
$this->pressEditorButton('Source');
|
||||
$this->assertNotNull($assert->waitForElementVisible('css', '.ck-source-editing-area'));
|
||||
$this->assertTrue($this->isSourceEditingScrollable());
|
||||
|
||||
// Double the editor row count.
|
||||
\Drupal::service('entity_display.repository')->getFormDisplay('node', 'page')
|
||||
->setComponent('body', [
|
||||
'type' => 'text_textarea_with_summary',
|
||||
'settings' => [
|
||||
'rows' => 18,
|
||||
],
|
||||
])
|
||||
->save();
|
||||
// Check the height of the editor again.
|
||||
$this->drupalGet('/node/add/page');
|
||||
$this->waitForEditor();
|
||||
// We expect height to be 640, but test to ensure that it's greater
|
||||
// than 600. We want to ensure that we don't hard code a very specific
|
||||
// value because tests might break if styles change (line-height, etc).
|
||||
// Note that the default height for CKEditor5 is 47px.
|
||||
$this->assertGreaterThan(600, $this->getEditorHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of ckeditor.
|
||||
*/
|
||||
private function getEditorHeight(bool $sourceEditing = FALSE): int {
|
||||
$selector = $sourceEditing ? '.ck-source-editing-area' : '.ck-editor__editable';
|
||||
$javascript = <<<JS
|
||||
return document.querySelector('$selector').clientHeight;
|
||||
JS;
|
||||
return $this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the window height.
|
||||
*/
|
||||
private function getWindowHeight(): int {
|
||||
$javascript = <<<JS
|
||||
return window.innerHeight;
|
||||
JS;
|
||||
return $this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the source editing element is scrollable.
|
||||
*/
|
||||
private function isSourceEditingScrollable(): bool {
|
||||
$javascript = <<<JS
|
||||
(function () {
|
||||
const element = document.querySelector('.ck-source-editing-area textarea');
|
||||
const style = window.getComputedStyle(element);
|
||||
if (
|
||||
element.scrollHeight > element.clientHeight &&
|
||||
style.overflow !== 'hidden' &&
|
||||
style['overflow-y'] !== 'hidden' &&
|
||||
style.overflow !== 'clip' &&
|
||||
style['overflow-y'] !== 'clip'
|
||||
) {
|
||||
if (
|
||||
element === document.scrollingElement ||
|
||||
(style.overflow !== 'visible' &&
|
||||
style['overflow-y'] !== 'visible')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
JS;
|
||||
$evaluateScript = $this->getSession()->evaluateScript($javascript);
|
||||
return $evaluateScript;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
// cspell:ignore esque māori sourceediting splitbutton upcasted
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5MarkupTest extends CKEditor5TestBase {
|
||||
|
||||
use TestFileCreationTrait;
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'media_library',
|
||||
'language',
|
||||
];
|
||||
|
||||
/**
|
||||
* Ensures that attribute values are encoded.
|
||||
*/
|
||||
public function testAttributeEncoding(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'ckeditor5',
|
||||
'name' => 'CKEditor 5 with image upload',
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'ckeditor5',
|
||||
'editor' => 'ckeditor5',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => ['drupalInsertImage'],
|
||||
],
|
||||
'plugins' => ['ckeditor5_imageResize' => ['allow_resize' => FALSE]],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => NULL,
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('ckeditor5'),
|
||||
FilterFormat::load('ckeditor5')
|
||||
))
|
||||
));
|
||||
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->waitForEditor();
|
||||
$page->fillField('title[0][value]', 'My test content');
|
||||
|
||||
// Ensure that CKEditor 5 is focused.
|
||||
$this->click('.ck-content');
|
||||
|
||||
$this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
|
||||
$image = $this->getTestFiles('image')[0];
|
||||
$image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
|
||||
$assert_session->waitForElementVisible('css', '.ck-widget.image');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-balloon-panel .ck-text-alternative-form'));
|
||||
$alt_override_input = $page->find('css', '.ck-balloon-panel .ck-text-alternative-form input[type=text]');
|
||||
$this->assertSame('', $alt_override_input->getValue());
|
||||
$alt_override_input->setValue('</em> Kittens & llamas are cute');
|
||||
$this->getBalloonButton('Save')->click();
|
||||
$page->pressButton('Save');
|
||||
|
||||
$uploaded_image = File::load(1);
|
||||
$image_uuid = $uploaded_image->uuid();
|
||||
$image_url = $this->container->get('file_url_generator')->generateString($uploaded_image->getFileUri());
|
||||
$this->drupalGet('node/1');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('xpath', sprintf('//img[@alt="</em> Kittens & llamas are cute" and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_uuid)));
|
||||
|
||||
// Drupal CKEditor 5 integrations overrides the CKEditor 5 HTML writer to
|
||||
// escape ampersand characters (&) and the angle brackets (< and >). This is
|
||||
// required because \Drupal\Component\Utility\Xss::filter fails to parse
|
||||
// element attributes with unescaped entities in value.
|
||||
// @see https://www.drupal.org/project/drupal/issues/3227831
|
||||
$this->assertEquals(sprintf('<img data-entity-uuid="%s" data-entity-type="file" src="%s" width="40" height="20" alt="</em> Kittens & llamas are cute">', $image_uuid, $image_url), Node::load(1)->get('body')->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that CKEditor 5 retains filter_html's allowed global attributes.
|
||||
*
|
||||
* FilterHtml always forbids the `style` and `on*` attributes, and always
|
||||
* allows the `lang` attribute (with any value) and the `dir` attribute (with
|
||||
* either `ltr` or `rtl` as value). It's important that those last two
|
||||
* attributes are guaranteed to be retained.
|
||||
*
|
||||
* @see \Drupal\filter\Plugin\Filter\FilterHtml::getHTMLRestrictions()
|
||||
* @see ckeditor5_globalAttributeDir
|
||||
* @see ckeditor5_globalAttributeLang
|
||||
* @see https://html.spec.whatwg.org/multipage/dom.html#global-attributes
|
||||
*/
|
||||
public function testFilterHtmlAllowedGlobalAttributes(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Add a node with text rendered via the Plain Text format.
|
||||
$this->drupalGet('node/add/page');
|
||||
$page->fillField('title[0][value]', 'Multilingual Hello World');
|
||||
// cSpell:disable-next-line
|
||||
$page->fillField('body[0][value]', '<p dir="ltr" lang="en">Hello World</p><p dir="rtl" lang="ar">مرحبا بالعالم</p>');
|
||||
$page->pressButton('Save');
|
||||
|
||||
$this->addNewTextFormat();
|
||||
|
||||
$this->drupalGet('node/1/edit');
|
||||
$page->selectFieldOption('body[0][format]', 'ckeditor5');
|
||||
$this->assertNotEmpty($assert_session->waitForText('Change text format?'));
|
||||
$page->pressButton('Continue');
|
||||
|
||||
$this->waitForEditor();
|
||||
$page->pressButton('Save');
|
||||
|
||||
// cSpell:disable-next-line
|
||||
$assert_session->responseContains('<p dir="ltr" lang="en">Hello World</p><p dir="rtl" lang="ar">مرحبا بالعالم</p>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that HTML comments are preserved in CKEditor 5.
|
||||
*/
|
||||
public function testComments(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Add a node with text rendered via the Plain Text format.
|
||||
$this->drupalGet('node/add');
|
||||
$page->fillField('title[0][value]', 'My test content');
|
||||
$page->fillField('body[0][value]', '<!-- Hamsters, alpacas, llamas, and kittens are cute! --><p>This is a <em>test!</em></p>');
|
||||
$page->pressButton('Save');
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'ckeditor5',
|
||||
'name' => 'CKEditor 5 HTML comments test',
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'ckeditor5',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('ckeditor5'),
|
||||
FilterFormat::load('ckeditor5')
|
||||
))
|
||||
));
|
||||
|
||||
$this->drupalGet('node/1/edit');
|
||||
$page->selectFieldOption('body[0][format]', 'ckeditor5');
|
||||
$this->assertNotEmpty($assert_session->waitForText('Change text format?'));
|
||||
$page->pressButton('Continue');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
$page->pressButton('Save');
|
||||
|
||||
$assert_session->responseContains('<!-- Hamsters, alpacas, llamas, and kittens are cute! --><p>This is a <em>test!</em></p>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that HTML scripts and styles are properly preserved in CKEditor 5.
|
||||
*/
|
||||
public function testStylesAndScripts(): void {
|
||||
$test_cases = [
|
||||
// Test cases taken from the HTML documentation.
|
||||
// @see https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
|
||||
'script' => [
|
||||
'<script>(function() { let x = 10, y = 5; if( y <--x ) { console.log("run me!"); }})()</script>',
|
||||
'<script>(function() { let x = 10, y = 5; if( y <--x ) { console.log("run me!"); }})()</script>',
|
||||
],
|
||||
'script like tag' => [
|
||||
'<script>(function() { let player = 5, script = 10; if (player<script) { console.log("run me!"); }})()</script>',
|
||||
'<script>(function() { let player = 5, script = 10; if (player<script) { console.log("run me!"); }})()</script>',
|
||||
],
|
||||
'script to escape' => [
|
||||
"<script>const example = 'Consider this string: <!-- <script>';</script>",
|
||||
"<script>const example = 'Consider this string: <!-- <script>';</script>",
|
||||
],
|
||||
'unescaped script tag' => [
|
||||
<<<HTML
|
||||
<script>
|
||||
const example = 'Consider this string: <!-- <script>';
|
||||
console.log(example);
|
||||
</script>
|
||||
<!-- despite appearances, this is actually part of the script still! -->
|
||||
<script>
|
||||
let a = 1 + 2; // this is the same script block still...
|
||||
</script>
|
||||
HTML,
|
||||
<<<HTML
|
||||
<script>
|
||||
const example = 'Consider this string: <!-- <script>';
|
||||
console.log(example);
|
||||
</script>
|
||||
<!-- despite appearances, this is actually part of the script still! -->
|
||||
<script>
|
||||
let a = 1 + 2; // this is the same script block still...
|
||||
</script>
|
||||
HTML,
|
||||
],
|
||||
'style' => [
|
||||
<<<HTML
|
||||
<style>
|
||||
a > span {
|
||||
/* Important comment. */
|
||||
color: red !important;
|
||||
}
|
||||
</style>
|
||||
HTML,
|
||||
<<<HTML
|
||||
<style>
|
||||
a > span {
|
||||
/* Important comment. */
|
||||
color: red !important;
|
||||
}
|
||||
</style>
|
||||
HTML,
|
||||
],
|
||||
'script and style' => [
|
||||
<<<HTML
|
||||
<script type="text/javascript">
|
||||
let x = 10;
|
||||
let y = 5;
|
||||
if(y < x){
|
||||
console.log('is smaller')
|
||||
}
|
||||
</script>
|
||||
<style type="text/css">
|
||||
:root {
|
||||
--main-bg-color: brown;
|
||||
}
|
||||
.sections > .section {
|
||||
background: var(--main-bg-color);
|
||||
}
|
||||
</style>
|
||||
HTML,
|
||||
<<<HTML
|
||||
<script type="text/javascript">
|
||||
let x = 10;
|
||||
let y = 5;
|
||||
if(y < x){
|
||||
console.log('is smaller')
|
||||
}
|
||||
</script><style type="text/css">
|
||||
:root {
|
||||
--main-bg-color: brown;
|
||||
}
|
||||
.sections > .section {
|
||||
background: var(--main-bg-color);
|
||||
}
|
||||
</style>
|
||||
HTML,
|
||||
],
|
||||
];
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Create filter.
|
||||
FilterFormat::create([
|
||||
'format' => 'ckeditor5',
|
||||
'name' => 'CKEditor 5 HTML',
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'ckeditor5',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'sourceEditing',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('ckeditor5'),
|
||||
FilterFormat::load('ckeditor5')
|
||||
))
|
||||
));
|
||||
|
||||
// Add a node with text rendered via the CKEditor 5 HTML format.
|
||||
foreach ($test_cases as $test_case_name => $test_case) {
|
||||
[$markup, $expected_content] = $test_case;
|
||||
$this->drupalGet('node/add');
|
||||
$page->fillField('title[0][value]', "Style and script test - $test_case_name");
|
||||
$this->waitForEditor();
|
||||
$this->pressEditorButton('Source');
|
||||
$editor = $page->find('css', '.ck-source-editing-area textarea');
|
||||
$editor->setValue($markup);
|
||||
$page->pressButton('Save');
|
||||
|
||||
$assert_session->responseContains($expected_content);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5 to ensure correct styling in off-canvas.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5OffCanvasTest extends CKEditor5TestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'ckeditor5',
|
||||
'ckeditor5_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests if CKEditor is properly styled inside an off-canvas dialog.
|
||||
*/
|
||||
public function testOffCanvasStyles(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->addNewTextFormat();
|
||||
|
||||
$this->drupalGet('/ckeditor5_test/off_canvas');
|
||||
|
||||
// The "Add Node" link triggers an off-canvas dialog with an add node form
|
||||
// that includes CKEditor.
|
||||
$page->clickLink('Add Node');
|
||||
$assert_session->waitForElementVisible('css', '#drupal-off-canvas-wrapper');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
$styles = $assert_session->elementExists('css', 'style#ckeditor5-off-canvas-reset');
|
||||
$this->assertStringContainsString('#drupal-off-canvas-wrapper [data-drupal-ck-style-fence]', $styles->getHtml());
|
||||
|
||||
$assert_session->elementExists('css', '.ck');
|
||||
|
||||
$ckeditor_toolbar_bg_color = $this->getSession()->evaluateScript('window.getComputedStyle(document.querySelector(\'.ck.ck-toolbar\')).backgroundColor');
|
||||
$this->assertEquals('rgb(255, 255, 255)', $ckeditor_toolbar_bg_color, 'Toolbar background-color should be unaffected by off-canvas');
|
||||
// Editable area should be visible.
|
||||
$assert_session->elementExists('css', '.ck .ck-content');
|
||||
$ckeditor_editable_bg_color = $this->getSession()->evaluateScript('window.getComputedStyle(document.querySelector(\'.ck.ck-content\')).backgroundColor');
|
||||
$this->assertEquals('rgb(255, 255, 255)', $ckeditor_editable_bg_color, 'Content background-color should be unaffected by off-canvas');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
|
||||
/**
|
||||
* Tests read-only mode for CKEditor 5.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5ReadOnlyModeTest extends CKEditor5TestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5_read_only_mode',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => 'field_second_ckeditor5_field',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'text_with_summary',
|
||||
'cardinality' => 1,
|
||||
]);
|
||||
$field_storage->save();
|
||||
|
||||
// Attach an instance of the field to the page content type.
|
||||
FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'page',
|
||||
'label' => 'Second CKEditor5 field',
|
||||
])->save();
|
||||
$this->container->get('entity_display.repository')
|
||||
->getFormDisplay('node', 'page')
|
||||
->setComponent('field_second_ckeditor5_field', [
|
||||
'type' => 'text_textarea_with_summary',
|
||||
])
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that disabling a CKEditor 5 field results in an uneditable editor.
|
||||
*/
|
||||
public function testReadOnlyMode(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$this->addNewTextFormat();
|
||||
|
||||
// Check that both CKEditor 5 fields are editable.
|
||||
$this->drupalGet('node/add');
|
||||
$assert_session->elementAttributeContains('css', '.field--name-body .ck-editor .ck-content', 'contenteditable', 'true');
|
||||
$assert_session->elementAttributeContains('css', '.field--name-field-second-ckeditor5-field .ck-editor .ck-content', 'contenteditable', 'true');
|
||||
|
||||
$this->container->get('state')->set('ckeditor5_read_only_mode_body_enabled', TRUE);
|
||||
|
||||
// Check that the first body field is no longer editable.
|
||||
$this->drupalGet('node/add');
|
||||
$assert_session->elementAttributeContains('css', '.field--name-body .ck-editor .ck-content', 'contenteditable', 'false');
|
||||
$assert_session->elementAttributeContains('css', '.field--name-field-second-ckeditor5-field .ck-editor .ck-content', 'contenteditable', 'true');
|
||||
|
||||
$this->container->get('state')->set('ckeditor5_read_only_mode_second_ckeditor5_field_enabled', TRUE);
|
||||
|
||||
// Both fields are disabled, check that both fields are no longer editable.
|
||||
$this->drupalGet('node/add');
|
||||
$assert_session->elementAttributeContains('css', '.field--name-body .ck-editor .ck-content', 'contenteditable', 'false');
|
||||
$assert_session->elementAttributeContains('css', '.field--name-field-second-ckeditor5-field .ck-editor .ck-content', 'contenteditable', 'false');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,729 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\Core\Language\LanguageManager;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
// cspell:ignore esque māori sourceediting splitbutton upcasted
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5Test extends CKEditor5TestBase {
|
||||
|
||||
use TestFileCreationTrait;
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'media_library',
|
||||
'language',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests configuring CKEditor 5 for existing content.
|
||||
*/
|
||||
public function testExistingContent(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Add a node with text rendered via the Plain Text format.
|
||||
$this->drupalGet('node/add/page');
|
||||
$page->fillField('title[0][value]', 'My test content');
|
||||
$page->fillField('body[0][value]', '<p>This is test content</p>');
|
||||
$page->pressButton('Save');
|
||||
$assert_session->responseNotContains('<p>This is test content</p>');
|
||||
$assert_session->responseContains('<p>This is test content</p>');
|
||||
|
||||
$this->addNewTextFormat();
|
||||
|
||||
// Change the node to use the new text format.
|
||||
$this->drupalGet('node/1/edit');
|
||||
|
||||
$page->selectFieldOption('body[0][format]', 'ckeditor5');
|
||||
$this->assertNotEmpty($assert_session->waitForText('Change text format?'));
|
||||
$page->pressButton('Continue');
|
||||
// Ensure the editor is loaded.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
$page->pressButton('Save');
|
||||
|
||||
// Assert that the HTML is rendered correctly.
|
||||
$assert_session->responseContains('<p>This is test content</p>');
|
||||
$assert_session->responseNotContains('<p>This is test content</p>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test headings configuration.
|
||||
*/
|
||||
public function testHeadingsPlugin(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->addNewTextFormat();
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', '<br> <p> <h2> <h3> <h4> <h5> <h6> <strong> <em>');
|
||||
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-heading-dropdown button'));
|
||||
|
||||
$page->find('css', '.ck-heading-dropdown button')->click();
|
||||
|
||||
// Get all the headings available in dropdown.
|
||||
$headings_dropdown = $page->findAll('css', '.ck-heading-dropdown li .ck-button__label');
|
||||
|
||||
// Create array of available headings.
|
||||
$available_headings = [];
|
||||
foreach ($headings_dropdown as $item) {
|
||||
$available_headings[] = $item->getText();
|
||||
}
|
||||
|
||||
$this->assertSame([
|
||||
'Paragraph',
|
||||
'Heading 2',
|
||||
'Heading 3',
|
||||
'Heading 4',
|
||||
'Heading 5',
|
||||
'Heading 6',
|
||||
], $available_headings);
|
||||
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$this->assertTrue($page->hasUncheckedField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading1]'));
|
||||
$page->checkField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading1]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertTrue($page->hasCheckedField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading1]'));
|
||||
$this->assertTrue($page->hasCheckedField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading2]'));
|
||||
$page->uncheckField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading2]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertTrue($page->hasUncheckedField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading2]'));
|
||||
$this->assertTrue($page->hasCheckedField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading4]'));
|
||||
$page->uncheckField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading4]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertTrue($page->hasUncheckedField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading4]'));
|
||||
$this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', '<br> <p> <h1> <h3> <h5> <h6> <strong> <em>');
|
||||
$this->assertTrue($page->hasUncheckedField('editor[settings][plugins][ckeditor5_heading][enabled_headings][heading4]'));
|
||||
|
||||
$page->pressButton('Save configuration');
|
||||
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-heading-dropdown button'));
|
||||
|
||||
$page->find('css', '.ck-heading-dropdown button')->click();
|
||||
|
||||
// Get all the headings available in dropdown.
|
||||
$headings_dropdown = $page->findAll('css', '.ck-heading-dropdown li .ck-button__label');
|
||||
|
||||
// Create array of available headings.
|
||||
$available_headings = [];
|
||||
foreach ($headings_dropdown as $item) {
|
||||
$available_headings[] = $item->getText();
|
||||
}
|
||||
|
||||
$this->assertSame([
|
||||
'Paragraph',
|
||||
'Heading 1',
|
||||
'Heading 3',
|
||||
'Heading 5',
|
||||
'Heading 6',
|
||||
], $available_headings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for Language of Parts plugin.
|
||||
*/
|
||||
public function testLanguageOfPartsPlugin(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->languageOfPartsPluginInitialConfigurationHelper($page, $assert_session);
|
||||
|
||||
// Test for "United Nations' official languages" option.
|
||||
$languages = LanguageManager::getUnitedNationsLanguageList();
|
||||
$this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'un');
|
||||
$this->languageOfPartsPluginTestHelper($page, $assert_session, $languages);
|
||||
|
||||
// Test for "Drupal predefined languages" option.
|
||||
$languages = LanguageManager::getStandardLanguageList();
|
||||
$this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'all');
|
||||
$this->languageOfPartsPluginTestHelper($page, $assert_session, $languages);
|
||||
|
||||
// Test for "Site-configured languages" option.
|
||||
ConfigurableLanguage::createFromLangcode('ar')->save();
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
ConfigurableLanguage::createFromLangcode('mi')->setName('Māori')->save();
|
||||
$configured_languages = \Drupal::languageManager()->getLanguages();
|
||||
$languages = [];
|
||||
foreach ($configured_languages as $language) {
|
||||
$language_name = $language->getName();
|
||||
$language_code = $language->getId();
|
||||
$languages[$language_code] = [$language_name];
|
||||
}
|
||||
$this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'site_configured');
|
||||
$this->languageOfPartsPluginTestHelper($page, $assert_session, $languages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to configure CKEditor5 with Language plugin.
|
||||
*/
|
||||
public function languageOfPartsPluginInitialConfigurationHelper($page, $assert_session): void {
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
// Press arrow down key to add the button to the active toolbar.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-textPartLanguage'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-textPartLanguage', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The CKEditor 5 module should warn that `<span>` cannot be created.
|
||||
$assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="warning"]:contains("The Language plugin needs another plugin to create <span>, for it to be able to create the following attributes: <span lang dir>. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.")');
|
||||
|
||||
// Make `<span>` creatable.
|
||||
$this->assertNotEmpty($assert_session->elementExists('css', '.ckeditor5-toolbar-item-sourceEditing'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
// The Source Editing plugin settings form should now be present and should
|
||||
// have no allowed tags configured.
|
||||
$page->clickLink('Source editing');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]'));
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]');
|
||||
allowedTags.value = '<span>';
|
||||
allowedTags.dispatchEvent(new Event('input'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
// Dispatching an `input` event does not work in WebDriver. Enabling another
|
||||
// toolbar item which has no associated HTML elements forces it.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-undo', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Confirm there are no longer any warnings.
|
||||
$assert_session->waitForElementRemoved('css', '[data-drupal-messages] [role="alert"]');
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->responseContains('Added text format <em class="placeholder">ckeditor5</em>.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to set language list option for CKEditor.
|
||||
*/
|
||||
public function languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, $option): void {
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', 'a[href^="#edit-editor-settings-plugins-ckeditor5-language"]'));
|
||||
|
||||
// Set correct value.
|
||||
$vertical_tab_link = $page->find('xpath', "//ul[contains(@class, 'vertical-tabs__menu')]/li/a[starts-with(@href, '#edit-editor-settings-plugins-ckeditor5-language')]");
|
||||
$vertical_tab_link->click();
|
||||
$select = $page->findField('editor[settings][plugins][ckeditor5_language][language_list]');
|
||||
if ($select->getValue() !== $option) {
|
||||
$select->selectOption($option);
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
}
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->responseContains('The text format <em class="placeholder">ckeditor5</em> has been updated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate expected languages available in editor.
|
||||
*/
|
||||
public function languageOfPartsPluginTestHelper($page, $assert_session, $configured_languages): void {
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->assertNotEmpty($assert_session->waitForText('Choose language'));
|
||||
|
||||
// Click on the dropdown button.
|
||||
$page->find('css', '.ck-text-fragment-language-dropdown button')->click();
|
||||
|
||||
// Get all the languages available in dropdown.
|
||||
$current_languages = $page->findAll('css', '.ck-text-fragment-language-dropdown li .ck-button__label');
|
||||
|
||||
// Remove "Remove language" element from current languages.
|
||||
array_shift($current_languages);
|
||||
|
||||
// Create array of full language name.
|
||||
$languages = [];
|
||||
foreach ($current_languages as $item) {
|
||||
$languages[] = $item->getText();
|
||||
}
|
||||
|
||||
// Return the values from a single column.
|
||||
$configured_languages = array_column($configured_languages, 0);
|
||||
|
||||
// Sort on full language name.
|
||||
asort($configured_languages);
|
||||
$this->assertSame(array_values($configured_languages), $languages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the titles of the vertical tabs in the given container.
|
||||
*
|
||||
* @param string $container_selector
|
||||
* The container in which to look for vertical tabs.
|
||||
* @param bool $visible_only
|
||||
* (optional) Whether to restrict to only the visible vertical tabs. TRUE by
|
||||
* default.
|
||||
*
|
||||
* @return string[]
|
||||
* The titles of all vertical tabs menu items, restricted to only
|
||||
* visible ones by default.
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
private function getVerticalTabs(string $container_selector, bool $visible_only = TRUE): array {
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
// Ensure the container exists.
|
||||
$container = $page->find('css', $container_selector);
|
||||
if ($container === NULL) {
|
||||
throw new \LogicException('The given container should exist.');
|
||||
}
|
||||
|
||||
// Make sure that the container selector contains exactly one Vertical Tabs
|
||||
// UI component.
|
||||
$vertical_tabs = $container->findAll('css', '.vertical-tabs');
|
||||
if (count($vertical_tabs) != 1) {
|
||||
throw new \LogicException('The given container should contain exactly one Vertical Tabs component.');
|
||||
}
|
||||
|
||||
$vertical_tabs = $container->findAll('css', '.vertical-tabs__menu-item');
|
||||
$vertical_tabs_titles = [];
|
||||
foreach ($vertical_tabs as $vertical_tab) {
|
||||
if ($visible_only && !$vertical_tab->isVisible()) {
|
||||
continue;
|
||||
}
|
||||
$title = $vertical_tab->find('css', '.vertical-tabs__menu-item-title')->getHtml();
|
||||
// When retrieving visible vertical tabs, mark the selected one.
|
||||
if ($visible_only && $vertical_tab->hasClass('is-selected')) {
|
||||
$title = "➡️$title";
|
||||
}
|
||||
$vertical_tabs_titles[] = $title;
|
||||
}
|
||||
return $vertical_tabs_titles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a disabled CKEditor 5 toolbar item.
|
||||
*
|
||||
* @param string $toolbar_item_id
|
||||
* The toolbar item to enable.
|
||||
*/
|
||||
protected function enableDisabledToolbarItem(string $toolbar_item_id): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$assert_session->elementExists('css', ".ckeditor5-toolbar-disabled .ckeditor5-toolbar-item-$toolbar_item_id");
|
||||
$this->triggerKeyUp(".ckeditor5-toolbar-item-$toolbar_item_id", 'ArrowDown');
|
||||
$assert_session->elementNotExists('css', ".ckeditor5-toolbar-disabled .ckeditor5-toolbar-item-$toolbar_item_id");
|
||||
$assert_session->elementExists('css', ".ckeditor5-toolbar-active .ckeditor5-toolbar-item-$toolbar_item_id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms active tab status is intact after AJAX refresh.
|
||||
*/
|
||||
public function testActiveTabsMaintained(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
// Initial vertical tabs: 3 for filters, 1 for CKE5 plugins.
|
||||
$this->assertSame([
|
||||
'Limit allowed HTML tags and correct faulty HTML',
|
||||
'Convert URLs into links',
|
||||
'Embed media',
|
||||
], $this->getVerticalTabs('#filter-settings-wrapper', FALSE));
|
||||
$this->assertSame([
|
||||
'Headings',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper', FALSE));
|
||||
|
||||
// Initial visible vertical tabs: 1 for filters, 1 for CKE5 plugins.
|
||||
$this->assertSame([
|
||||
'➡️Limit allowed HTML tags and correct faulty HTML',
|
||||
], $this->getVerticalTabs('#filter-settings-wrapper'));
|
||||
$this->assertSame([
|
||||
'➡️Headings',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper'));
|
||||
|
||||
// Enable media embed to make a second filter config vertical tab visible.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[media_embed][status]'));
|
||||
$this->assertNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0));
|
||||
$page->checkField('filters[media_embed][status]');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector=edit-filters-media-embed-settings]', 0));
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
// Filter plugins vertical tabs behavior: the filter plugin settings
|
||||
// vertical tab with the heaviest filter weight is active by default.
|
||||
// Hence enabling the media_embed filter (weight 100) results in its
|
||||
// vertical tab being activated (filter_html's weight is -10).
|
||||
// @see core/modules/filter/filter.admin.js
|
||||
$this->assertSame([
|
||||
'Limit allowed HTML tags and correct faulty HTML',
|
||||
'➡️Embed media',
|
||||
], $this->getVerticalTabs('#filter-settings-wrapper'));
|
||||
$this->assertSame([
|
||||
'➡️Headings',
|
||||
'Media',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper'));
|
||||
|
||||
// Enable upload image to add a third (and fourth) CKE5 plugin vertical tab.
|
||||
$this->enableDisabledToolbarItem('drupalInsertImage');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
// The active CKE5 plugin settings vertical tab is unchanged.
|
||||
$this->assertSame([
|
||||
'➡️Headings',
|
||||
'Image',
|
||||
'Image resize',
|
||||
'Media',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper'));
|
||||
// The active filter plugin settings vertical tab is unchanged.
|
||||
$this->assertSame([
|
||||
'Limit allowed HTML tags and correct faulty HTML',
|
||||
'➡️Embed media',
|
||||
], $this->getVerticalTabs('#filter-settings-wrapper'));
|
||||
|
||||
// Open the CKE5 "Image" plugin settings vertical tab, interact with the
|
||||
// subform and observe that the AJAX requests those interactions trigger do
|
||||
// not change the active vertical tabs.
|
||||
$page->clickLink('Image');
|
||||
$assert_session->waitForText('Enable image uploads');
|
||||
$this->assertSame([
|
||||
'Headings',
|
||||
'➡️Image',
|
||||
'Image resize',
|
||||
'Media',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper'));
|
||||
$this->assertTrue($page->hasUncheckedField('editor[settings][plugins][ckeditor5_image][status]'));
|
||||
$page->checkField('editor[settings][plugins][ckeditor5_image][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertSame([
|
||||
'Headings',
|
||||
'➡️Image',
|
||||
'Image resize',
|
||||
'Media',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper'));
|
||||
$this->assertSame([
|
||||
'Limit allowed HTML tags and correct faulty HTML',
|
||||
'➡️Embed media',
|
||||
], $this->getVerticalTabs('#filter-settings-wrapper'));
|
||||
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextContains('Added text format ckeditor5');
|
||||
|
||||
// Leave and return to the config form, wait for initialized Vertical Tabs.
|
||||
$this->drupalGet('admin/config/content/formats/');
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$assert_session->waitForElement('css', '.vertical-tabs__menu-item.is-selected');
|
||||
|
||||
// The first CKE5 plugin settings vertical tab is active by default.
|
||||
$this->assertSame([
|
||||
'➡️Headings',
|
||||
'Image',
|
||||
'Image resize',
|
||||
'Media',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper'));
|
||||
// Filter plugins vertical tabs behavior: the filter plugin settings
|
||||
// vertical tab with the heaviest filter weight is active by default.
|
||||
// Hence enabling the media_embed filter (weight 100) results in its
|
||||
// vertical tab being activated (filter_html's weight is -10).
|
||||
// @see core/modules/filter/filter.admin.js
|
||||
$this->assertSame([
|
||||
'Limit allowed HTML tags and correct faulty HTML',
|
||||
'➡️Embed media',
|
||||
], $this->getVerticalTabs('#filter-settings-wrapper'));
|
||||
|
||||
// Click the 3rd CKE5 plugin vertical tab.
|
||||
$page->clickLink($this->getVerticalTabs('#plugin-settings-wrapper')[2]);
|
||||
$this->assertSame([
|
||||
'Headings',
|
||||
'Image',
|
||||
'➡️Image resize',
|
||||
'Media',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper'));
|
||||
|
||||
// Add another CKEditor 5 toolbar item just to trigger an AJAX refresh.
|
||||
$this->enableDisabledToolbarItem('blockQuote');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
// The active CKE5 plugin settings vertical tab is unchanged.
|
||||
$this->assertSame([
|
||||
'Headings',
|
||||
'Image',
|
||||
'➡️Image resize',
|
||||
'Media',
|
||||
], $this->getVerticalTabs('#plugin-settings-wrapper'));
|
||||
// The active filter plugin settings vertical tab is unchanged.
|
||||
$this->assertSame([
|
||||
'Limit allowed HTML tags and correct faulty HTML',
|
||||
'➡️Embed media',
|
||||
], $this->getVerticalTabs('#filter-settings-wrapper'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that CKEditor 5 integrates with file reference filter.
|
||||
*/
|
||||
public function testEditorFileReferenceIntegration(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-drupalInsertImage'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$page->clickLink('Image');
|
||||
$page->checkField('editor[settings][plugins][ckeditor5_image][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$page->checkField('filters[editor_file_reference][status]');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->saveNewTextFormat($page, $assert_session);
|
||||
|
||||
$this->drupalGet('node/add/page');
|
||||
$page->fillField('title[0][value]', 'My test content');
|
||||
|
||||
// Ensure that CKEditor 5 is focused.
|
||||
$this->click('.ck-content');
|
||||
|
||||
$this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
|
||||
$image = $this->getTestFiles('image')[0];
|
||||
$image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
|
||||
// Wait until preview for the image has rendered to ensure that the image
|
||||
// upload has completed and the image has been downcast.
|
||||
// @see https://www.drupal.org/project/drupal/issues/3250587
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-content img[data-entity-uuid]'));
|
||||
|
||||
// Add alt text to the image.
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.image.ck-widget > img'));
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-balloon-panel .ck-text-alternative-form'));
|
||||
$alt_override_input = $page->find('css', '.ck-balloon-panel .ck-text-alternative-form input[type=text]');
|
||||
$alt_override_input->setValue('There is now alt text');
|
||||
$this->getBalloonButton('Save')->click();
|
||||
$page->pressButton('Save');
|
||||
|
||||
$uploaded_image = File::load(1);
|
||||
$image_url = $this->container->get('file_url_generator')->generateString($uploaded_image->getFileUri());
|
||||
$image_uuid = $uploaded_image->uuid();
|
||||
$assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @width="40" and @height="20" and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid));
|
||||
|
||||
// Ensure that width, height, and length attributes are not stored in the
|
||||
// database.
|
||||
$this->assertEquals(sprintf('<img data-entity-uuid="%s" data-entity-type="file" src="%s" width="40" height="20" alt="There is now alt text">', $image_uuid, $image_url), Node::load(1)->get('body')->value);
|
||||
|
||||
// Ensure that data-entity-uuid and data-entity-type attributes are upcasted
|
||||
// correctly to CKEditor model.
|
||||
$this->drupalGet('node/1/edit');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
$page->pressButton('Save');
|
||||
|
||||
$assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @width="40" and @height="20" and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that CKEditor italic model is converted to em.
|
||||
*/
|
||||
public function testEmphasis(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Add a node with text rendered via the Plain Text format.
|
||||
$this->drupalGet('node/add/page');
|
||||
$page->fillField('title[0][value]', 'My test content');
|
||||
$page->fillField('body[0][value]', '<p>This is a <em>test!</em></p>');
|
||||
$page->pressButton('Save');
|
||||
|
||||
$this->addNewTextFormat();
|
||||
|
||||
$this->drupalGet('node/1/edit');
|
||||
$page->selectFieldOption('body[0][format]', 'ckeditor5');
|
||||
$this->assertNotEmpty($assert_session->waitForText('Change text format?'));
|
||||
$page->pressButton('Continue');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
$page->pressButton('Save');
|
||||
|
||||
$assert_session->responseContains('<p>This is a <em>test!</em></p>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests list plugin.
|
||||
*/
|
||||
public function testListPlugin(): void {
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'CKEditor 5 with list',
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'test_format',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => ['sourceEditing', 'numberedList'],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_list' => [
|
||||
'properties' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
$ordered_list_html = '<ol><li>apple</li><li>banana</li><li>cantaloupe</li></ol>';
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
$this->drupalGet('node/add/page');
|
||||
$page->fillField('title[0][value]', 'My test content');
|
||||
$this->pressEditorButton('Source');
|
||||
$source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea');
|
||||
$source_text_area->setValue($ordered_list_html);
|
||||
// Click source again to make source inactive and have the numbered list
|
||||
// splitbutton active.
|
||||
$this->pressEditorButton('Source');
|
||||
$numbered_list_dropdown_selector = '.ck-splitbutton__arrow';
|
||||
|
||||
// Check that there is no dropdown available for the numbered list because
|
||||
// both reversed and startIndex are FALSE.
|
||||
$assert_session->elementNotExists('css', $numbered_list_dropdown_selector);
|
||||
// Save content so source content is kept after changing the editor config.
|
||||
$page->pressButton('Save');
|
||||
$edit_url = $this->getSession()->getCurrentURL() . '/edit';
|
||||
$this->drupalGet($edit_url);
|
||||
$this->waitForEditor();
|
||||
|
||||
// Enable the reversed functionality.
|
||||
$editor = Editor::load('test_format');
|
||||
$settings = $editor->getSettings();
|
||||
$settings['plugins']['ckeditor5_list']['properties']['reversed'] = TRUE;
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$this->getSession()->reload();
|
||||
$this->waitForEditor();
|
||||
$this->click($numbered_list_dropdown_selector);
|
||||
$reversed_order_button_selector = '.ck.ck-button.ck-numbered-list-properties__reversed-order';
|
||||
$assert_session->elementExists('css', $reversed_order_button_selector);
|
||||
$assert_session->elementTextEquals('css', $reversed_order_button_selector, 'Reversed order');
|
||||
$start_index_element_selector = '.ck.ck-numbered-list-properties__start-index';
|
||||
$assert_session->elementNotExists('css', $start_index_element_selector);
|
||||
|
||||
// Have both the reversed and the start index enabled.
|
||||
$editor = Editor::load('test_format');
|
||||
$settings = $editor->getSettings();
|
||||
$settings['plugins']['ckeditor5_list']['properties']['startIndex'] = TRUE;
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$this->getSession()->reload();
|
||||
$this->waitForEditor();
|
||||
$this->click($numbered_list_dropdown_selector);
|
||||
$assert_session->elementExists('css', $reversed_order_button_selector);
|
||||
$assert_session->elementTextEquals('css', $reversed_order_button_selector, 'Reversed order');
|
||||
$assert_session->elementExists('css', $start_index_element_selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that changes are saved in CKEditor 5.
|
||||
*/
|
||||
public function testSave(): void {
|
||||
// To replicate the bug from https://www.drupal.org/i/3396742
|
||||
// We need 2 or more text formats and node edit page.
|
||||
FilterFormat::create([
|
||||
'format' => 'ckeditor5',
|
||||
'name' => 'CKEditor 5 HTML',
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'ckeditor5',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'sourceEditing',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('ckeditor5'),
|
||||
FilterFormat::load('ckeditor5')
|
||||
))
|
||||
));
|
||||
FilterFormat::create([
|
||||
'format' => 'ckeditor5_2',
|
||||
'name' => 'CKEditor 5 HTML 2',
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'ckeditor5_2',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('ckeditor5_2'),
|
||||
FilterFormat::load('ckeditor5_2')
|
||||
))
|
||||
));
|
||||
$this->drupalCreateNode([
|
||||
'title' => 'My test content',
|
||||
]);
|
||||
|
||||
// Test that entered text is saved.
|
||||
$this->drupalGet('node/1/edit');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->waitForEditor();
|
||||
$editor = $page->find('css', '.ck-content');
|
||||
$editor->setValue('Very important information');
|
||||
$page->pressButton('Save');
|
||||
$this->assertSession()->responseContains('Very important information');
|
||||
|
||||
// Test that changes only in source are saved.
|
||||
$this->drupalGet('node/1/edit');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->waitForEditor();
|
||||
$this->pressEditorButton('Source');
|
||||
$editor = $page->find('css', '.ck-source-editing-area textarea');
|
||||
$editor->setValue('Text hidden in the source');
|
||||
$page->pressButton('Save');
|
||||
$this->assertSession()->responseContains('Text hidden in the source');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Behat\Mink\Element\TraversableElement;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\user\RoleInterface;
|
||||
|
||||
// cspell:ignore esque
|
||||
|
||||
/**
|
||||
* Base class for testing CKEditor 5.
|
||||
*
|
||||
* @ingroup testing
|
||||
* @internal
|
||||
*/
|
||||
abstract class CKEditor5TestBase extends WebDriverTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'ckeditor5',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'administer filters',
|
||||
'create page content',
|
||||
'edit own page content',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add and save a new text format using CKEditor 5.
|
||||
*/
|
||||
protected function addNewTextFormat($name = 'ckeditor5'): void {
|
||||
FilterFormat::create([
|
||||
'format' => $name,
|
||||
'roles' => [RoleInterface::AUTHENTICATED_ID],
|
||||
'name' => $name,
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<br> <p> <h2> <h3> <h4> <h5> <h6> <strong> <em>',
|
||||
],
|
||||
],
|
||||
'filter_align' => ['status' => TRUE],
|
||||
'filter_caption' => ['status' => TRUE],
|
||||
],
|
||||
])->save();
|
||||
|
||||
Editor::create([
|
||||
'editor' => $name,
|
||||
'format' => $name,
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new text format using CKEditor 5.
|
||||
*/
|
||||
public function createNewTextFormat($page, $assert_session, $name = 'ckeditor5') {
|
||||
$this->drupalGet('admin/config/content/formats/add');
|
||||
$page->fillField('name', $name);
|
||||
$assert_session->waitForText('Machine name');
|
||||
$this->assertNotEmpty($assert_session->waitForText($name));
|
||||
$page->checkField('roles[authenticated]');
|
||||
|
||||
if ($name === 'ckeditor5') {
|
||||
// Enable the HTML filter, at least one HTML restricting filter is needed
|
||||
// before CKEditor 5 can be enabled.
|
||||
$this->assertTrue($page->hasUncheckedField('filters[filter_html][status]'));
|
||||
$page->checkField('filters[filter_html][status]');
|
||||
|
||||
// Add the tags that must be included in the html filter for CKEditor 5.
|
||||
$allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$allowed_html_field->setValue('<p> <br>');
|
||||
}
|
||||
$page->selectFieldOption('editor[editor]', $name);
|
||||
$assert_session->assertExpectedAjaxRequest(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the new text format.
|
||||
*/
|
||||
public function saveNewTextFormat($page, $assert_session) {
|
||||
$page->pressButton('Save configuration');
|
||||
$this->assertTrue($assert_session->waitForText('Added text format'), "Confirm new text format saved");
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a keyup event on the selected element.
|
||||
*
|
||||
* @param string $selector
|
||||
* The css selector for the element.
|
||||
* @param string $key
|
||||
* The keyCode.
|
||||
*/
|
||||
protected function triggerKeyUp(string $selector, string $key) {
|
||||
|
||||
$script = <<<JS
|
||||
(function (selector, key) {
|
||||
const btn = document.querySelector(selector);
|
||||
btn.dispatchEvent(new KeyboardEvent('keydown', { key }));
|
||||
btn.dispatchEvent(new KeyboardEvent('keyup', { key }));
|
||||
})('{$selector}', '{$key}')
|
||||
|
||||
JS;
|
||||
|
||||
$options = [
|
||||
'script' => $script,
|
||||
'args' => [],
|
||||
];
|
||||
|
||||
$this->getSession()->getDriver()->getWebDriverSession()->execute($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates ::fieldValueEquals() to force DrupalCI to provide useful errors.
|
||||
*
|
||||
* @param string $field
|
||||
* Field id|name|label|value.
|
||||
* @param string $value
|
||||
* Field value.
|
||||
* @param \Behat\Mink\Element\TraversableElement $container
|
||||
* Document to check against.
|
||||
*
|
||||
* @throws \Behat\Mink\Exception\ExpectationException
|
||||
*
|
||||
* @see \Behat\Mink\WebAssert::fieldValueEquals()
|
||||
*/
|
||||
protected function assertHtmlEsqueFieldValueEquals($field, $value, ?TraversableElement $container = NULL) {
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$node = $assert_session->fieldExists($field, $container);
|
||||
$actual = $node->getValue();
|
||||
$regex = '/^' . preg_quote($value, '/') . '$/ui';
|
||||
|
||||
$message = sprintf('The field "%s" value is "%s", but "%s" expected.', $field, htmlspecialchars($actual), htmlspecialchars($value));
|
||||
|
||||
$assert_session->assert((bool) preg_match($regex, $actual), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that no real-time validation errors are present.
|
||||
*
|
||||
* @throws \Behat\Mink\Exception\ElementNotFoundException
|
||||
*/
|
||||
protected function assertNoRealtimeValidationErrors(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertSame('', $assert_session->elementExists('css', '[data-drupal-selector="ckeditor5-realtime-validation-messages-container"]')->getHtml());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\user\Entity\User;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5 editor UI with Toolbar module.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5ToolbarTest extends WebDriverTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'node',
|
||||
'ckeditor5',
|
||||
'toolbar',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* The admin user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected User $user;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
|
||||
$this->drupalCreateContentType([
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
]);
|
||||
|
||||
$this->user = $this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'access toolbar',
|
||||
'edit any article content',
|
||||
'administer site configuration',
|
||||
]);
|
||||
$this->drupalLogin($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that CKEditor 5 toolbar renders below Drupal Toolbar.
|
||||
*/
|
||||
public function test(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Create test content to ensure that CKEditor 5 text editor can be
|
||||
// scrolled.
|
||||
$body = '';
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$body .= '<p>' . $this->randomMachineName(32) . '</p>';
|
||||
}
|
||||
$edit_url = $this->drupalCreateNode(['type' => 'article', 'body' => ['value' => $body, 'format' => 'test_format']])->toUrl('edit-form');
|
||||
$this->drupalGet($edit_url);
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '#toolbar-bar'));
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
|
||||
// Ensure the body has enough height to enable scrolling. Scroll 110px from
|
||||
// top of body field to ensure CKEditor 5 toolbar is sticky.
|
||||
$this->getSession()->evaluateScript('document.body.style.height = "10000px";');
|
||||
$this->getSession()->evaluateScript('location.hash = "#edit-body-0-value";');
|
||||
$this->getSession()->evaluateScript('scroll(0, document.documentElement.scrollTop + 110);');
|
||||
// Focus CKEditor 5 text editor.
|
||||
$javascript = <<<JS
|
||||
Drupal.CKEditor5Instances.get(document.getElementById("edit-body-0-value").dataset["ckeditor5Id"]).editing.view.focus();
|
||||
JS;
|
||||
$this->getSession()->evaluateScript($javascript);
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-sticky-panel__placeholder'));
|
||||
$toolbar_height = (int) $this->getSession()->evaluateScript('document.getElementById("toolbar-bar").offsetHeight');
|
||||
$ckeditor5_toolbar_position = (int) $this->getSession()->evaluateScript("document.querySelector('.ck-toolbar').getBoundingClientRect().top");
|
||||
$this->assertEqualsWithDelta($toolbar_height, $ckeditor5_toolbar_position, 2);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* Tests emphasis in CKEditor 5.
|
||||
*
|
||||
* CKEditor's use of <i> is converted to <em> in Drupal, so additional coverage
|
||||
* is provided here to verify successful conversion.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class EmphasisTest extends WebDriverTestBase {
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* The user to use during testing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* A host entity with a body field to use the <em> tag in.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
'node',
|
||||
'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <br> <em>',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'italic',
|
||||
'sourceEditing',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
]);
|
||||
|
||||
$this->drupalCreateContentType(['type' => 'blog']);
|
||||
$this->host = $this->createNode([
|
||||
'type' => 'blog',
|
||||
'title' => 'Animals with strange names',
|
||||
'body' => [
|
||||
'value' => '<p>This is a <em>test!</em></p>',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that CKEditor italic model is converted to em.
|
||||
*/
|
||||
public function testEmphasis(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
$emphasis_element = $assert_session->waitForElementVisible('css', '.ck-content p em');
|
||||
$this->assertEquals('test!', $emphasis_element->getText());
|
||||
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$emphasis_source = $xpath->query('//p/em');
|
||||
$this->assertNotEmpty($emphasis_source);
|
||||
$this->assertEquals('test!', $emphasis_source[0]->textContent);
|
||||
$page->pressButton('Save');
|
||||
|
||||
$assert_session->responseContains('<p>This is a <em>test!</em></p>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that arbitrary attributes are allowed via GHS.
|
||||
*/
|
||||
public function testEmphasisArbitraryHtml(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$editor = Editor::load('test_format');
|
||||
$settings = $editor->getSettings();
|
||||
|
||||
// Allow the data-foo attribute in img via GHS.
|
||||
$settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = ['<em data-foo>'];
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
|
||||
// Add data-foo use to an existing em tag.
|
||||
$original_value = $this->host->body->value;
|
||||
$this->host->body->value = str_replace('<em>', '<em data-foo="bar">', $original_value);
|
||||
$this->host->save();
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
$emphasis_element = $assert_session->waitForElementVisible('css', '.ck-content p em');
|
||||
$this->assertEquals('bar', $emphasis_element->getAttribute('data-foo'));
|
||||
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query('//em[@data-foo="bar"]'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
// cspell:ignore imageresize imageupload
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image
|
||||
* @group ckeditor5
|
||||
* @group #slow
|
||||
* @internal
|
||||
*/
|
||||
class ImageTest extends ImageTestTestBase {
|
||||
use ImageTestBaselineTrait;
|
||||
|
||||
/**
|
||||
* Tests the ckeditor5_imageResize and ckeditor5_imageUpload settings forms.
|
||||
*/
|
||||
public function testImageSettingsForm(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->drupalGet('admin/config/content/formats/manage/test_format');
|
||||
|
||||
// The image resize and upload plugin settings forms should be present.
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-image"]');
|
||||
|
||||
// Removing the drupalImageInsert button from the toolbar must remove the
|
||||
// plugin settings forms too.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage', 'ArrowUp');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-image"]');
|
||||
|
||||
// Re-adding the drupalImageInsert button to the toolbar must re-add the
|
||||
// plugin settings forms too.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-drupalInsertImage', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
|
||||
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-image"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that it's possible to upload SVG image, with the test module enabled.
|
||||
*/
|
||||
public function testCanUploadSvg(): void {
|
||||
$this->container->get('module_installer')
|
||||
->install(['ckeditor5_test_module_allowed_image']);
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$src = 'core/modules/ckeditor5/tests/fixtures/test-svg-upload.svg';
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
$this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
|
||||
$image_upload_field->attachFile($this->container->get('file_system')->realpath($src));
|
||||
// Wait for the image to be uploaded and rendered by CKEditor 5.
|
||||
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '.ck-widget.image-inline > img[src$="test-svg-upload.svg"]'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
|
||||
// cspell:ignore imageresize
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
abstract class ImageTestBase extends CKEditor5TestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
use TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* The user to use during testing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* A host entity with a body field to embed images in.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
'node',
|
||||
'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Provides the relevant image attributes.
|
||||
*
|
||||
* @return string[]
|
||||
* An associative array with the image source, width, and height.
|
||||
*/
|
||||
protected function imageAttributes() {
|
||||
return [
|
||||
'src' => base_path() . 'core/misc/druplicon.png',
|
||||
'width' => '88',
|
||||
'height' => '100',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format attributes.
|
||||
*
|
||||
* @param bool $reverse
|
||||
* Reverse attributes when printing them.
|
||||
*
|
||||
* @return string
|
||||
* A space-separated string of image attributes.
|
||||
*/
|
||||
protected function imageAttributesAsString($reverse = FALSE) {
|
||||
$string = [];
|
||||
foreach ($this->imageAttributes() as $key => $value) {
|
||||
$string[] = $key . '="' . $value . '"';
|
||||
}
|
||||
if ($reverse) {
|
||||
$string = array_reverse($string);
|
||||
}
|
||||
return implode(' ', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an image to the CKEditor 5 editable zone.
|
||||
*/
|
||||
protected function addImage() {
|
||||
$page = $this->getSession()->getPage();
|
||||
$src = $this->imageAttributes()['src'];
|
||||
$this->waitForEditor();
|
||||
$this->pressEditorButton('Insert image via URL');
|
||||
$dialog = $page->find('css', '.ck-dialog');
|
||||
$src_input = $dialog->find('css', '.ck-image-insert-url input[type=text]');
|
||||
$src_input->setValue($src);
|
||||
$dialog->find('xpath', "//button[span[text()='Insert']]")->click();
|
||||
// Wait for the image to be uploaded and rendered by CKEditor 5.
|
||||
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '.ck-widget.image > img[src="' . $src . '"]'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
// cspell:ignore imageresize
|
||||
|
||||
/**
|
||||
* Trait with common test methods for image tests.
|
||||
*/
|
||||
trait ImageTestBaselineTrait {
|
||||
|
||||
/**
|
||||
* Ensures that attributes are retained on conversion.
|
||||
*/
|
||||
public function testAttributeRetentionDuringUpcasting(): void {
|
||||
// Run test cases in a single test to make the test run faster.
|
||||
$attributes_to_retain = [
|
||||
'-none-' => 'inline',
|
||||
'data-caption="test caption 🦙"' => 'block',
|
||||
'data-align="left"' => 'inline',
|
||||
];
|
||||
|
||||
foreach ($attributes_to_retain as $attribute_to_retain => $expected_upcast_behavior_when_wrapped_in_block_element) {
|
||||
if ($attribute_to_retain === '-none-') {
|
||||
$attribute_to_retain = '';
|
||||
}
|
||||
$img_tag = '<img ' . $attribute_to_retain . ' alt="drupalimage test image" ' . $this->imageAttributesAsString() . ' />';
|
||||
$test_cases = [
|
||||
// Plain image tag for a baseline.
|
||||
[
|
||||
$img_tag,
|
||||
$img_tag,
|
||||
],
|
||||
// Image tag wrapped with <p>.
|
||||
[
|
||||
"<p>$img_tag</p>",
|
||||
$expected_upcast_behavior_when_wrapped_in_block_element === 'inline' ? "<p>$img_tag</p>" : $img_tag,
|
||||
],
|
||||
// Image tag wrapped with a disallowed paragraph-like element (<div).
|
||||
// When inline is the expected upcast behavior, it will wrap in <p>
|
||||
// because it still must wrap in a paragraph-like element, and <p> is
|
||||
// available to be that element.
|
||||
[
|
||||
"<div>$img_tag</div>",
|
||||
$expected_upcast_behavior_when_wrapped_in_block_element === 'inline' ? "<p>$img_tag</p>" : $img_tag,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($test_cases as $test_case) {
|
||||
[$markup, $expected] = $test_case;
|
||||
$this->host->body->value = $markup;
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
// Ensure that the image is rendered in preview.
|
||||
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', ".ck-content .ck-widget img"));
|
||||
$editor_dom = $this->getEditorDataAsDom();
|
||||
$expected_dom = Html::load($expected);
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertEquals($expected_dom->getElementsByTagName('body')->item(0)->C14N(), $editor_dom->getElementsByTagName('body')->item(0)->C14N());
|
||||
|
||||
// Ensure the test attribute is persisted on downcast.
|
||||
if ($attribute_to_retain) {
|
||||
$this->assertNotEmpty($xpath->query("//img[@$attribute_to_retain]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that arbitrary attributes are allowed via GHS.
|
||||
*/
|
||||
public function testImageArbitraryHtml(): void {
|
||||
$editor = Editor::load('test_format');
|
||||
$settings = $editor->getSettings();
|
||||
|
||||
// Allow the data-foo attribute in img via GHS.
|
||||
$settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = ['<img data-foo>'];
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
$format = FilterFormat::load('test_format');
|
||||
$original_config = $format->filters('filter_html')
|
||||
->getConfiguration();
|
||||
|
||||
foreach ($this->providerLinkability() as $data) {
|
||||
[$image_type, $unrestricted] = $data;
|
||||
|
||||
$format_config = $unrestricted ? ['status' => FALSE] : $original_config;
|
||||
$format->setFilterConfig('filter_html', $format_config)
|
||||
->save();
|
||||
|
||||
// Make the test content have either a block image or an inline image.
|
||||
$img_tag = '<img data-foo="bar" alt="drupalimage test image" data-entity-type="file" ' . $this->imageAttributesAsString() . ' />';
|
||||
$this->host->body->value .= $image_type === 'block'
|
||||
? $img_tag
|
||||
: "<p>$img_tag</p>";
|
||||
$this->host->save();
|
||||
|
||||
$expected_widget_selector = $image_type === 'block' ? 'image img' : 'image-inline';
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
$drupalimage = $this->assertSession()->waitForElementVisible('css', ".ck-content .ck-widget.$expected_widget_selector");
|
||||
$this->assertNotEmpty($drupalimage);
|
||||
$this->assertEquals('bar', $drupalimage->getAttribute('data-foo'));
|
||||
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query('//img[@data-foo="bar"]'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests linkability of the image CKEditor widget.
|
||||
*
|
||||
* Due to the complex overrides that `drupalImage.DrupalImage` is making, this
|
||||
* is explicitly testing the "editingDowncast" and "dataDowncast" results.
|
||||
* These are CKEditor 5 concepts.
|
||||
*
|
||||
* @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#conversion
|
||||
*/
|
||||
public function testLinkability(): void {
|
||||
$format = FilterFormat::load('test_format');
|
||||
$original_config = $format->filters('filter_html')
|
||||
->getConfiguration();
|
||||
$original_body_value = $this->host->body->value;
|
||||
foreach ($this->providerLinkability() as $data) {
|
||||
[$image_type, $unrestricted] = $data;
|
||||
assert($image_type === 'inline' || $image_type === 'block');
|
||||
|
||||
$format_config = $unrestricted ? ['status' => FALSE] : $original_config;
|
||||
|
||||
$format->setFilterConfig('filter_html', $format_config)
|
||||
->save();
|
||||
|
||||
// Make the test content have either a block image or an inline image.
|
||||
$img_tag = '<img alt="drupalimage test image" data-entity-type="file" ' . $this->imageAttributesAsString() . ' />';
|
||||
$this->host->body->value = $original_body_value . ($image_type === 'block'
|
||||
? $img_tag
|
||||
: "<p>$img_tag</p>");
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$page = $this->getSession()->getPage();
|
||||
// Adjust the expectations accordingly.
|
||||
$expected_widget_class = $image_type === 'block' ? 'image' : 'image-inline';
|
||||
|
||||
$this->waitForEditor();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Initial state: the image CKEditor Widget is not selected.
|
||||
$drupalimage = $assert_session->waitForElementVisible('css', ".ck-content .ck-widget.$expected_widget_class");
|
||||
$this->assertNotEmpty($drupalimage);
|
||||
$this->assertFalse($drupalimage->hasClass('.ck-widget_selected'));
|
||||
|
||||
$src = basename($this->imageAttributes()['src']);
|
||||
// Assert the "editingDowncast" HTML before making changes.
|
||||
$assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]');
|
||||
|
||||
// Assert the "dataDowncast" HTML before making changes.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query('//img[@alt="drupalimage test image"]'));
|
||||
$this->assertEmpty($xpath->query('//a'));
|
||||
|
||||
// Assert the link button is present and not pressed.
|
||||
$link_button = $this->getEditorButton('Link');
|
||||
$this->assertSame('false', $link_button->getAttribute('aria-pressed'));
|
||||
|
||||
// Tests linking images.
|
||||
$drupalimage->click();
|
||||
$this->assertTrue($drupalimage->hasClass('ck-widget_selected'));
|
||||
$this->assertEditorButtonEnabled('Link');
|
||||
// Assert structure of image toolbar balloon.
|
||||
$this->assertVisibleBalloon('.ck-toolbar[aria-label="Image toolbar"]');
|
||||
$link_image_button = $this->getBalloonButton('Link image');
|
||||
// Click the "Link image" button.
|
||||
$this->assertSame('false', $link_image_button->getAttribute('aria-pressed'));
|
||||
$link_image_button->press();
|
||||
// Assert structure of link form balloon.
|
||||
$balloon = $this->assertVisibleBalloon('.ck-link-form');
|
||||
$url_input = $balloon->find('css', '.ck-labeled-field-view__input-wrapper .ck-input-text[inputmode=url]');
|
||||
// Fill in link form balloon's <input> and hit "Insert".
|
||||
$url_input->setValue('http://www.drupal.org/association');
|
||||
$balloon->pressButton('Insert');
|
||||
|
||||
// Assert the "editingDowncast" HTML after making changes. First assert
|
||||
// the link exists, then assert the expected DOM structure in detail.
|
||||
$assert_session->elementExists('css', '.ck-content a[href*="//www.drupal.org/association"]');
|
||||
// For inline images, the link is wrapping the widget; for block images
|
||||
// the link lives inside the widget. (This is how it is implemented
|
||||
// upstream, it could be implemented differently, we just want to ensure
|
||||
// we do not break it. Drupal only cares about having its own
|
||||
// "dataDowncast", the "editingDowncast" is considered an implementation
|
||||
// detail.)
|
||||
$assert_session->elementExists('css', $image_type === 'inline'
|
||||
? '.ck-content a[href*="//www.drupal.org/association"] .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]'
|
||||
: '.ck-content .ck-widget.' . $expected_widget_class . ' a[href*="//www.drupal.org/association"] > img[src*="' . $src . '"][alt="drupalimage test image"]'
|
||||
);
|
||||
|
||||
// Assert the "dataDowncast" HTML after making changes.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertCount(1, $xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]'));
|
||||
$this->assertEmpty($xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]'));
|
||||
|
||||
// Add `class="trusted"` to the link.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertEmpty($xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]'));
|
||||
$this->pressEditorButton('Source');
|
||||
$source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea');
|
||||
$this->assertNotEmpty($source_text_area);
|
||||
$new_value = str_replace('<a ', '<a class="trusted" ', $source_text_area->getValue());
|
||||
$source_text_area->setValue('<p>temp</p>');
|
||||
$source_text_area->setValue($new_value);
|
||||
$this->pressEditorButton('Source');
|
||||
|
||||
// When unrestricted, additional attributes on links should be retained.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]'));
|
||||
|
||||
// Save the entity whose text field is being edited.
|
||||
$page->pressButton('Save');
|
||||
|
||||
// Assert the HTML the end user sees.
|
||||
$assert_session->elementExists('css', $unrestricted
|
||||
? 'a[href="http://www.drupal.org/association"].trusted img[src*="' . $src . '"]'
|
||||
: 'a[href="http://www.drupal.org/association"] img[src*="' . $src . '"]');
|
||||
|
||||
// Go back to edit the now *linked* <drupal-media>. Everything from this
|
||||
// point onwards is effectively testing "upcasting" and proving there is
|
||||
// no data loss.
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
// Assert the "dataDowncast" HTML before making changes.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query('//img[@alt="drupalimage test image"]'));
|
||||
$this->assertNotEmpty($xpath->query('//a[@href="http://www.drupal.org/association"]'));
|
||||
$this->assertNotEmpty($xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]'));
|
||||
$this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://www.drupal.org/association" and @class="trusted"]'));
|
||||
|
||||
// Tests unlinking images.
|
||||
$drupalimage->click();
|
||||
$this->assertEditorButtonEnabled('Link');
|
||||
$this->assertSame('true', $this->getEditorButton('Link')->getAttribute('aria-pressed'));
|
||||
// Assert structure of image toolbar balloon.
|
||||
$this->assertVisibleBalloon('.ck-toolbar[aria-label="Image toolbar"]');
|
||||
$link_image_button = $this->getBalloonButton('Link image');
|
||||
$this->assertSame('true', $link_image_button->getAttribute('aria-pressed'));
|
||||
$link_image_button->click();
|
||||
// Assert structure of link actions balloon.
|
||||
$this->getBalloonButton('Edit link');
|
||||
$unlink_image_button = $this->getBalloonButton('Unlink');
|
||||
// Click the "Unlink" button.
|
||||
$unlink_image_button->click();
|
||||
$this->assertSame('false', $this->getEditorButton('Link')->getAttribute('aria-pressed'));
|
||||
|
||||
// Assert the "editingDowncast" HTML after making changes. Assert the
|
||||
// widget exists but not the link, or *any* link for that matter. Then
|
||||
// assert the expected DOM structure in detail.
|
||||
$assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class);
|
||||
$assert_session->elementNotExists('css', '.ck-content a');
|
||||
$assert_session->elementExists('css', '.ck-content .ck-widget.' . $expected_widget_class . ' > img[src*="' . $src . '"][alt="drupalimage test image"]');
|
||||
|
||||
// Assert the "dataDowncast" HTML after making changes.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertCount(0, $xpath->query('//a[@href="http://www.drupal.org/association"]/img[@alt="drupalimage test image"]'));
|
||||
$this->assertCount(1, $xpath->query('//img[@alt="drupalimage test image"]'));
|
||||
$this->assertCount(0, $xpath->query('//a'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data for testLinkability() and testImageArbitraryHtml().
|
||||
*/
|
||||
protected function providerLinkability(): array {
|
||||
return [
|
||||
'BLOCK image, restricted' => ['block', FALSE],
|
||||
'BLOCK image, unrestricted' => ['block', TRUE],
|
||||
'INLINE image, restricted' => ['inline', FALSE],
|
||||
'INLINE image, unrestricted' => ['inline', TRUE],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that images can have caption set.
|
||||
*/
|
||||
public function testImageCaption(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// The foo attribute is added to be removed later by CKEditor 5 to make sure
|
||||
// CKEditor 5 was able to downcast data.
|
||||
$img_tag = '<img ' . $this->imageAttributesAsString() . ' alt="drupalimage test image" data-caption="Alpacas <em>are</em> cute<br>really!" foo="bar">';
|
||||
$this->host->body->value = $img_tag;
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
$this->assertNotEmpty($figcaption = $assert_session->waitForElement('css', '.image figcaption'));
|
||||
$this->assertSame('Alpacas <em>are</em> cute<br>really!', $figcaption->getHtml());
|
||||
$page->pressButton('Source');
|
||||
$editor_dom = $this->getEditorDataAsDom();
|
||||
$data_caption = $editor_dom->getElementsByTagName('img')->item(0)->getAttribute('data-caption');
|
||||
$this->assertSame('Alpacas <em>are</em> cute<br>really!', $data_caption);
|
||||
|
||||
$page->pressButton('Save');
|
||||
|
||||
$src = $this->imageAttributes()['src'];
|
||||
$expected = '<img ' . $this->imageAttributesAsString(TRUE) . ' alt="drupalimage test image" data-caption="Alpacas <em>are</em> cute<br>really!">';
|
||||
$expected_dom = Html::load($expected);
|
||||
$this->assertEquals($expected_dom->getElementsByTagName('body')->item(0)->C14N(), $editor_dom->getElementsByTagName('body')->item(0)->C14N());
|
||||
$assert_session->elementExists('xpath', '//figure/img[@src="' . $src . '" and not(@data-caption)]');
|
||||
$assert_session->responseContains('<figcaption>Alpacas <em>are</em> cute<br>really!</figcaption>');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image
|
||||
* @group ckeditor5
|
||||
* @group #slow
|
||||
* @internal
|
||||
*/
|
||||
class ImageTestProviderTest extends ImageTestTestBase {
|
||||
use ImageTestProviderTrait;
|
||||
|
||||
}
|
||||
@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
// cspell:ignore imageresize
|
||||
|
||||
/**
|
||||
* Provides test methods using data providers for image tests.
|
||||
*/
|
||||
trait ImageTestProviderTrait {
|
||||
|
||||
/**
|
||||
* Tests that alt text is required for images.
|
||||
*
|
||||
* @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#conversion
|
||||
*
|
||||
* @dataProvider providerAltTextRequired
|
||||
*/
|
||||
public function testAltTextRequired(bool $unrestricted): void {
|
||||
// Disable filter_html.
|
||||
if ($unrestricted) {
|
||||
FilterFormat::load('test_format')
|
||||
->setFilterConfig('filter_html', ['status' => FALSE])
|
||||
->save();
|
||||
}
|
||||
|
||||
// Make the test content has a block image and an inline image.
|
||||
$img_tag = preg_replace(
|
||||
'/width="\d+" height="\d+"/',
|
||||
'width="500"',
|
||||
'<img ' . $this->imageAttributesAsString() . ' />'
|
||||
);
|
||||
$this->host->body->value .= $img_tag . "<p>$img_tag</p>";
|
||||
$this->host->save();
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Confirm both of the images exist.
|
||||
$this->assertNotEmpty($image_block = $assert_session->waitForElementVisible('css', ".ck-content .ck-widget.image"));
|
||||
$this->assertNotEmpty($image_inline = $assert_session->waitForElementVisible('css', ".ck-content .ck-widget.image-inline"));
|
||||
|
||||
// Confirm both of the images have an alt text required warning.
|
||||
$this->assertNotEmpty($image_block->find('css', '.image-alternative-text-missing-wrapper'));
|
||||
$this->assertNotEmpty($image_inline->find('css', '.image-alternative-text-missing-wrapper'));
|
||||
|
||||
// Add alt text to the block image.
|
||||
$image_block->find('css', '.image-alternative-text-missing button')->click();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-balloon-panel'));
|
||||
$this->assertVisibleBalloon('.ck-text-alternative-form');
|
||||
|
||||
// Ensure that the missing alt text warning is hidden when the alternative
|
||||
// text form is open.
|
||||
$assert_session->waitForElement('css', '.ck-content .ck-widget.image .image-alternative-text-missing.ck-hidden');
|
||||
$assert_session->elementExists('css', '.ck-content .ck-widget.image-inline .image-alternative-text-missing');
|
||||
$assert_session->elementNotExists('css', '.ck-content .ck-widget.image-inline .image-alternative-text-missing.ck-hidden');
|
||||
|
||||
// Ensure that the missing alt text error is not added to decorative images.
|
||||
$this->assertNotEmpty($decorative_button = $this->getBalloonButton('Decorative image'));
|
||||
$assert_session->elementExists('css', '.ck-balloon-panel .ck-text-alternative-form input[type=text]');
|
||||
$decorative_button->click();
|
||||
$assert_session->elementExists('css', '.ck-content .ck-widget.image .image-alternative-text-missing.ck-hidden');
|
||||
$assert_session->elementExists('css', ".ck-content .ck-widget.image-inline .image-alternative-text-missing-wrapper");
|
||||
$assert_session->elementNotExists('css', '.ck-content .ck-widget.image-inline .image-alternative-text-missing.ck-hidden');
|
||||
|
||||
// Ensure that the missing alt text error is removed after saving the
|
||||
// changes.
|
||||
$this->assertNotEmpty($save_button = $this->getBalloonButton('Save'));
|
||||
$save_button->click();
|
||||
$this->assertTrue($assert_session->waitForElementRemoved('css', ".ck-content .ck-widget.image .image-alternative-text-missing-wrapper"));
|
||||
$assert_session->elementExists('css', '.ck-content .ck-widget.image-inline .image-alternative-text-missing-wrapper');
|
||||
|
||||
// Ensure that the decorative image downcasts into empty alt attribute.
|
||||
$editor_dom = $this->getEditorDataAsDom();
|
||||
$decorative_img = $editor_dom->getElementsByTagName('img')->item(0);
|
||||
$this->assertTrue($decorative_img->hasAttribute('alt'));
|
||||
$this->assertEmpty($decorative_img->getAttribute('alt'));
|
||||
|
||||
// Ensure that missing alt text error is not added to images with alt text.
|
||||
$this->assertNotEmpty($alt_text_button = $this->getBalloonButton('Change image alternative text'));
|
||||
$alt_text_button->click();
|
||||
|
||||
$decorative_button->click();
|
||||
$this->assertNotEmpty($save_button = $this->getBalloonButton('Save'));
|
||||
$this->assertTrue($save_button->hasClass('ck-disabled'));
|
||||
|
||||
$this->assertNotEmpty($alt_override_input = $page->find('css', '.ck-balloon-panel .ck-text-alternative-form input[type=text]'));
|
||||
$alt_override_input->setValue('There is now alt text');
|
||||
$this->assertTrue($assert_session->waitForElementRemoved('css', '.ck-balloon-panel .ck-text-alternative-form .ck-disabled'));
|
||||
$this->assertFalse($save_button->hasClass('ck-disabled'));
|
||||
$save_button->click();
|
||||
|
||||
// Save the node and confirm that the alt text is retained.
|
||||
$page->pressButton('Save');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', 'img[alt="There is now alt text"]'));
|
||||
|
||||
// Ensure that alt form is opened after image upload.
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->addImage();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-text-alternative-form'));
|
||||
$this->assertVisibleBalloon('.ck-text-alternative-form');
|
||||
}
|
||||
|
||||
/**
|
||||
* Providers data for testAltTextRequired().
|
||||
*/
|
||||
public static function providerAltTextRequired(): array {
|
||||
return [
|
||||
'Restricted' => [FALSE],
|
||||
'Unrestricted' => [TRUE],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests alignment integration.
|
||||
*
|
||||
* @dataProvider providerAlignment
|
||||
*/
|
||||
public function testAlignment(string $image_type): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
// Make the test content have either a block image or an inline image.
|
||||
$img_tag = '<img alt="drupalimage test image" ' . $this->imageAttributesAsString() . ' />';
|
||||
$this->host->body->value .= $image_type === 'block'
|
||||
? $img_tag
|
||||
: "<p>$img_tag</p>";
|
||||
$this->host->save();
|
||||
|
||||
$image_selector = $image_type === 'block' ? '.ck-widget.image' : '.ck-widget.image-inline';
|
||||
$default_alignment = $image_type === 'block' ? 'Break text' : 'In line';
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', $image_selector));
|
||||
|
||||
// Ensure that the default alignment option matches expectation.
|
||||
$this->click($image_selector);
|
||||
$this->assertVisibleBalloon('[aria-label="Image toolbar"]');
|
||||
$this->assertTrue($this->getBalloonButton($default_alignment)->hasClass('ck-on'));
|
||||
$editor_dom = $this->getEditorDataAsDom();
|
||||
$drupal_media_element = $editor_dom->getElementsByTagName('img')
|
||||
->item(0);
|
||||
$this->assertFalse($drupal_media_element->hasAttribute('data-align'));
|
||||
$this->getBalloonButton('Align center and break text')->click();
|
||||
|
||||
// Assert the alignment class exists after editing downcast.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-widget.image.image-style-align-center'));
|
||||
$editor_dom = $this->getEditorDataAsDom();
|
||||
$drupal_media_element = $editor_dom->getElementsByTagName('img')
|
||||
->item(0);
|
||||
$this->assertEquals('center', $drupal_media_element->getAttribute('data-align'));
|
||||
|
||||
$page->pressButton('Save');
|
||||
// Check that the 'content has been updated' message status appears to
|
||||
// confirm we left the editor.
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '[data-drupal-messages]'));
|
||||
// Check that the class is correct in the front end.
|
||||
$assert_session->elementExists('css', 'img.align-center');
|
||||
// Go back to the editor to check that the alignment class still exists.
|
||||
$edit_url = $this->getSession()->getCurrentURL() . '/edit';
|
||||
$this->drupalGet($edit_url);
|
||||
$this->waitForEditor();
|
||||
$assert_session->elementExists('css', '.ck-widget.image.image-style-align-center');
|
||||
|
||||
// Ensure that "Centered image" alignment option is selected.
|
||||
$this->click('.ck-widget.image');
|
||||
$this->assertVisibleBalloon('[aria-label="Image toolbar"]');
|
||||
$this->assertTrue($this->getBalloonButton('Align center and break text')->hasClass('ck-on'));
|
||||
$this->getBalloonButton('Break text')->click();
|
||||
$this->assertTrue($assert_session->waitForElementRemoved('css', '.ck-widget.image.image-style-align-center'));
|
||||
$editor_dom = $this->getEditorDataAsDom();
|
||||
$drupal_media_element = $editor_dom->getElementsByTagName('img')
|
||||
->item(0);
|
||||
$this->assertFalse($drupal_media_element->hasAttribute('data-align'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testAlignment().
|
||||
*/
|
||||
public static function providerAlignment() {
|
||||
return [
|
||||
'Block image' => ['block'],
|
||||
'Inline image' => ['inline'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that width attribute upcasts and downcasts correctly.
|
||||
*
|
||||
* @param string $width
|
||||
* The width input for the image.
|
||||
*
|
||||
* @dataProvider providerWidth
|
||||
*/
|
||||
public function testWidth(string $width): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Despite the absence of a `height` attribute on the `<img>`, CKEditor 5
|
||||
// should generate an appropriate `height`, matching with the aspect ratio
|
||||
// of the image.
|
||||
$expected_computed_height = $width;
|
||||
if (!str_ends_with($width, '%')) {
|
||||
$ratio = $width / (int) $this->imageAttributes()['width'];
|
||||
$expected_computed_height = (string) (int) round($ratio * (int) $this->imageAttributes()['height']);
|
||||
}
|
||||
|
||||
// Add image to the host body.
|
||||
$this->host->body->value = sprintf('<img data-foo="bar" alt="drupalimage test image" ' . $this->imageAttributesAsString() . ' width="%s" />', $width);
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
// Ensure that the image is upcast as expected. In the editing view, the
|
||||
// width attribute should downcast to an inline style on the container
|
||||
// element.
|
||||
$assert_session->waitForElementVisible('css', ".ck-widget.image");
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', ".ck-widget.image[style] img"));
|
||||
|
||||
// Ensure that the width attribute is retained on downcast.
|
||||
$editor_data = $this->getEditorDataAsDom();
|
||||
$img_in_editor = $editor_data->getElementsByTagName('img')->item(0);
|
||||
$this->assertSame($width, $img_in_editor->getAttribute('width'));
|
||||
$this->assertSame($expected_computed_height, $img_in_editor->getAttribute('height'));
|
||||
|
||||
// Save the node and ensure that the width attribute is retained, and ensure
|
||||
// that a natural image ratio-respecting height attribute has been added.
|
||||
$page->pressButton('Save');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', "img[width='$width'][height='$expected_computed_height']"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testWidth().
|
||||
*
|
||||
* @return string[][]
|
||||
* An array of test cases, each with a width value.
|
||||
*/
|
||||
public static function providerWidth(): array {
|
||||
return [
|
||||
'Image resize with percent unit (only allowed in HTML 4)' => [
|
||||
'width' => '33%',
|
||||
],
|
||||
'Image resize with (implied) px unit' => [
|
||||
'width' => '100',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the image resize plugin.
|
||||
*
|
||||
* Confirms that enabling the resize plugin introduces the resize class to
|
||||
* images within CKEditor 5.
|
||||
*
|
||||
* @param bool $is_resize_enabled
|
||||
* Boolean flag to test enabled or disabled.
|
||||
*
|
||||
* @dataProvider providerResize
|
||||
*/
|
||||
public function testResize(bool $is_resize_enabled): void {
|
||||
// Disable resize plugin because it is enabled by default.
|
||||
if (!$is_resize_enabled) {
|
||||
Editor::load('test_format')->setSettings([
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'drupalInsertImage',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_imageResize' => [
|
||||
'allow_resize' => FALSE,
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
}
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
$this->drupalGet('node/add');
|
||||
$page->fillField('title[0][value]', 'My test content');
|
||||
$this->addImage();
|
||||
$selector = $is_resize_enabled ? 'figure.ck-widget_with-resizer' : 'figure:not(.ck-widget_with-resizer)';
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', $selector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testResize().
|
||||
*
|
||||
* @return array
|
||||
* The test cases.
|
||||
*/
|
||||
public static function providerResize(): array {
|
||||
return [
|
||||
'Image resize is enabled' => [
|
||||
'is_resize_enabled' => TRUE,
|
||||
],
|
||||
'Image resize is disabled' => [
|
||||
'is_resize_enabled' => FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
// cspell:ignore imageresize imageupload
|
||||
|
||||
/**
|
||||
* Provides a base class for testing CKEditor 5 image embedding and uploads.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class ImageTestTestBase extends ImageTestBase {
|
||||
|
||||
/**
|
||||
* The sample image File entity to embed.
|
||||
*
|
||||
* @var \Drupal\file\FileInterface
|
||||
*/
|
||||
protected $file;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <br> <em> <a href> <img src alt data-entity-uuid data-entity-type height width data-caption data-align>',
|
||||
],
|
||||
],
|
||||
'filter_align' => ['status' => TRUE],
|
||||
'filter_caption' => ['status' => TRUE],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'drupalInsertImage',
|
||||
'sourceEditing',
|
||||
'link',
|
||||
'italic',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
'ckeditor5_imageResize' => [
|
||||
'allow_resize' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => '1M',
|
||||
'max_dimensions' => ['width' => 100, 'height' => 100],
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
'administer filters',
|
||||
]);
|
||||
|
||||
// Create a sample host entity to embed images in.
|
||||
$this->file = File::create([
|
||||
'uri' => $this->getTestFiles('image')[0]->uri,
|
||||
]);
|
||||
$this->file->save();
|
||||
$this->host = $this->createNode([
|
||||
'type' => 'page',
|
||||
'title' => 'Animals with strange names',
|
||||
'body' => [
|
||||
'value' => '<p>The pirate is irate.</p>',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the relevant image attributes.
|
||||
*
|
||||
* @return string[]
|
||||
* Default image attributes for tests.
|
||||
*/
|
||||
protected function imageAttributes(): array {
|
||||
return [
|
||||
'data-entity-type' => 'file',
|
||||
'data-entity-uuid' => $this->file->uuid(),
|
||||
'src' => $this->file->createFileUrl(),
|
||||
'width' => '40',
|
||||
'height' => '20',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a test image.
|
||||
*/
|
||||
protected function addImage() {
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
|
||||
$image = $this->getTestFiles('image')[0];
|
||||
$image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
|
||||
// Wait for the image to be uploaded and rendered by CKEditor 5.
|
||||
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '.ck-widget.image > img[src*="' . $image->filename . '"]'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image
|
||||
* @group ckeditor5
|
||||
* @group #slow
|
||||
* @internal
|
||||
*/
|
||||
class ImageUrlProviderTest extends ImageUrlTestBase {
|
||||
use ImageTestProviderTrait;
|
||||
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Image
|
||||
* @group ckeditor5
|
||||
* @group #slow
|
||||
* @internal
|
||||
*/
|
||||
class ImageUrlTest extends ImageUrlTestBase {
|
||||
use ImageTestBaselineTrait;
|
||||
|
||||
/**
|
||||
* Tests the Drupal image URL widget.
|
||||
*/
|
||||
public function testImageUrlWidget(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$image_selector = '.ck-widget.image-inline';
|
||||
$src = $this->imageAttributes()['src'];
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
$this->pressEditorButton('Insert image via URL');
|
||||
$dialog = $page->find('css', '.ck-dialog');
|
||||
$src_input = $dialog->find('css', '.ck-image-insert-url input[type=text]');
|
||||
$src_input->setValue($src);
|
||||
$dialog->find('xpath', "//button[span[text()='Insert']]")->click();
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', $image_selector));
|
||||
$this->click($image_selector);
|
||||
$this->assertVisibleBalloon('[aria-label="Image toolbar"]');
|
||||
|
||||
$this->pressEditorButton('Update image URL');
|
||||
$dialog = $page->find('css', '.ck-dialog');
|
||||
$src_input = $dialog->find('css', '.ck-image-insert-url input[type=text]');
|
||||
$this->assertEquals($src, $src_input->getValue());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
// cspell:ignore imageresize
|
||||
|
||||
/**
|
||||
* Provides a base class for testing CKEditor 5 image URL insertion.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class ImageUrlTestBase extends ImageTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <br> <em> <a href> <img alt height width src data-caption data-align>',
|
||||
],
|
||||
],
|
||||
'filter_align' => ['status' => TRUE],
|
||||
'filter_caption' => ['status' => TRUE],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'drupalInsertImage',
|
||||
'sourceEditing',
|
||||
'link',
|
||||
'italic',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
'ckeditor5_imageResize' => [
|
||||
'allow_resize' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
'administer filters',
|
||||
]);
|
||||
|
||||
$this->host = $this->createNode([
|
||||
'type' => 'page',
|
||||
'title' => 'Animals with strange names',
|
||||
'body' => [
|
||||
'value' => '<p>The pirate is irate.</p>',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
|
||||
|
||||
// cspell:ignore drupalmediatoolbar
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5 plugins using Drupal's translation system.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class JSTranslationTest extends CKEditor5TestBase {
|
||||
|
||||
use MediaTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'locale',
|
||||
'media_library',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
// Create a sample media entity to be embedded.
|
||||
$this->createMediaType('image', ['id' => 'image', 'label' => 'Image']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration test to ensure that CKEditor 5 Plugins translations are loaded.
|
||||
*/
|
||||
public function test(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-drupalMedia'));
|
||||
$this->click('#edit-filters-media-embed-status');
|
||||
$assert_session->assertExpectedAjaxRequest(2);
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-drupalMedia', 'ArrowDown');
|
||||
$assert_session->assertExpectedAjaxRequest(3);
|
||||
$this->saveNewTextFormat($page, $assert_session);
|
||||
|
||||
$langcode = 'fr';
|
||||
ConfigurableLanguage::createFromLangcode($langcode)->save();
|
||||
$this->config('system.site')->set('default_langcode', $langcode)->save();
|
||||
|
||||
// Visit a page that will trigger a JavaScript file parsing for
|
||||
// translatable strings.
|
||||
$this->drupalGet('node/add');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
|
||||
// Ensure a string from the CKEditor 5 plugin is picked up by translation.
|
||||
// @see core/modules/ckeditor5/js/ckeditor5_plugins/drupalMedia/src/drupalmediatoolbar.js
|
||||
$locale_storage = $this->container->get('locale.storage');
|
||||
$string = $locale_storage->findString(['source' => 'Drupal Media toolbar', 'context' => '']);
|
||||
$this->assertNotEmpty($string, 'String from JavaScript file saved.');
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
// cspell:ignore คำพูดบล็อก sourceediting
|
||||
|
||||
/**
|
||||
* Tests for CKEditor 5 UI translations.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class LanguageTest extends CKEditor5TestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
'locale',
|
||||
];
|
||||
|
||||
/**
|
||||
* Integration test to ensure that CKEditor 5 UI translations are loaded.
|
||||
*
|
||||
* @param string $langcode
|
||||
* The language code.
|
||||
* @param string $toolbar_item_name
|
||||
* The CKEditor 5 plugin to enable.
|
||||
* @param string $toolbar_item_translation
|
||||
* The expected translation for CKEditor 5 plugin toolbar button.
|
||||
*
|
||||
* @dataProvider provider
|
||||
*/
|
||||
public function test(string $langcode, string $toolbar_item_name, string $toolbar_item_translation): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
// Special case: textPartLanguage toolbar item can only create `<span lang>`
|
||||
// but not `<span>`. The purpose of this test is to test translations, not
|
||||
// the configuration of the textPartLanguage functionality. So, make sure
|
||||
// that `<span>` can be created so we can test how UI translations work when
|
||||
// using `textPartLanguage`.
|
||||
if ($toolbar_item_name === 'textPartLanguage') {
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-sourceEditing'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The Source Editing plugin settings form should now be present and
|
||||
// should have no allowed tags configured.
|
||||
$page->clickLink('Source editing');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]'));
|
||||
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]');
|
||||
allowedTags.value = '<span>';
|
||||
allowedTags.dispatchEvent(new Event('input'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
}
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', ".ckeditor5-toolbar-item-$toolbar_item_name"));
|
||||
$this->triggerKeyUp(".ckeditor5-toolbar-item-$toolbar_item_name", 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->saveNewTextFormat($page, $assert_session);
|
||||
|
||||
ConfigurableLanguage::createFromLangcode($langcode)->save();
|
||||
$this->config('system.site')->set('default_langcode', $langcode)->save();
|
||||
|
||||
$this->drupalGet('node/add');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
// Ensure that blockquote button is translated.
|
||||
$assert_session->elementExists('xpath', "//span[text()='$toolbar_item_translation']");
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ensuring CKEditor 5 UI translations are loaded.
|
||||
*
|
||||
* @return string[][]
|
||||
* An array of language code, CKEditor 5 plugin name, and expected
|
||||
* translation.
|
||||
*/
|
||||
public static function provider(): array {
|
||||
return [
|
||||
'Language code both in Drupal and CKEditor' => [
|
||||
'langcode' => 'th',
|
||||
'toolbar_item_name' => 'blockQuote',
|
||||
'toolbar_item_translation' => 'คำพูดบล็อก',
|
||||
],
|
||||
'Language code transformed from browser mappings' => [
|
||||
'langcode' => 'zh-hans',
|
||||
'toolbar_item_name' => 'blockQuote',
|
||||
'toolbar_item_translation' => '块引用',
|
||||
],
|
||||
'Language configuration conflict' => [
|
||||
'langcode' => 'fr',
|
||||
'toolbar_item_name' => 'textPartLanguage',
|
||||
// cSpell:disable-next-line
|
||||
'toolbar_item_translation' => 'Choisir la langue',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\media\Entity\Media;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
// cspell:ignore arrakis complote détruire harkonnen
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\MediaLibrary
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class MediaLibraryTest extends WebDriverTestBase {
|
||||
|
||||
use MediaTypeCreationTrait;
|
||||
use TestFileCreationTrait;
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'starterkit_theme';
|
||||
|
||||
/**
|
||||
* The user to use during testing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* The media item to embed.
|
||||
*
|
||||
* @var \Drupal\media\MediaInterface
|
||||
*/
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
'media_library',
|
||||
'node',
|
||||
'media',
|
||||
'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'media_embed' => ['status' => TRUE],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'drupalMedia',
|
||||
'sourceEditing',
|
||||
'undo',
|
||||
'redo',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
'media_media' => [
|
||||
'allow_view_mode_override' => FALSE,
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
|
||||
$this->drupalCreateContentType(['type' => 'blog']);
|
||||
|
||||
// Note that media_install() grants 'view media' to all users by default.
|
||||
$this->user = $this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'access media overview',
|
||||
'create blog content',
|
||||
]);
|
||||
|
||||
// Create a media type that starts with the letter a, to test tab order.
|
||||
$this->createMediaType('image', ['id' => 'arrakis', 'label' => 'Arrakis']);
|
||||
|
||||
// Create a sample media entity to be embedded.
|
||||
$this->createMediaType('image', ['id' => 'image', 'label' => 'Image']);
|
||||
File::create([
|
||||
'uri' => $this->getTestFiles('image')[0]->uri,
|
||||
])->save();
|
||||
$this->media = Media::create([
|
||||
'bundle' => 'image',
|
||||
'name' => 'Fear is the mind-killer',
|
||||
'field_media_image' => [
|
||||
[
|
||||
'target_id' => 1,
|
||||
'alt' => 'default alt',
|
||||
'title' => 'default title',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->media->save();
|
||||
|
||||
$arrakis_media = Media::create([
|
||||
'bundle' => 'arrakis',
|
||||
'name' => 'Le baron Vladimir Harkonnen',
|
||||
'field_media_image' => [
|
||||
[
|
||||
'target_id' => 1,
|
||||
'alt' => 'Il complote pour détruire le duc Leto',
|
||||
'title' => 'Il complote pour détruire le duc Leto',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$arrakis_media->save();
|
||||
|
||||
$this->drupalLogin($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests using drupalMedia button to embed media into CKEditor 5.
|
||||
*/
|
||||
public function testButton(): void {
|
||||
$media_preview_selector = '.ck-content .ck-widget.drupal-media .media';
|
||||
$this->drupalGet('/node/add/blog');
|
||||
$this->waitForEditor();
|
||||
$this->pressEditorButton('Insert Media');
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-modal #media-library-content'));
|
||||
|
||||
// Ensure that the tab order is correct.
|
||||
$tabs = $page->findAll('css', '.media-library-menu__link');
|
||||
$expected_tab_order = [
|
||||
'Show Image media (selected)',
|
||||
'Show Arrakis media',
|
||||
];
|
||||
foreach ($tabs as $key => $tab) {
|
||||
$this->assertSame($expected_tab_order[$key], $tab->getText());
|
||||
}
|
||||
|
||||
$assert_session->elementExists('css', '.js-media-library-item')->click();
|
||||
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', $media_preview_selector, 1000));
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$drupal_media = $xpath->query('//drupal-media')[0];
|
||||
$expected_attributes = [
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => $this->media->uuid(),
|
||||
];
|
||||
foreach ($expected_attributes as $name => $expected) {
|
||||
$this->assertSame($expected, $drupal_media->getAttribute($name));
|
||||
}
|
||||
$this->assertEditorButtonEnabled('Undo');
|
||||
$this->pressEditorButton('Undo');
|
||||
$this->assertEmpty($assert_session->waitForElementVisible('css', $media_preview_selector, 1000));
|
||||
$this->assertEditorButtonDisabled('Undo');
|
||||
$this->pressEditorButton('Redo');
|
||||
$this->assertEditorButtonEnabled('Undo');
|
||||
|
||||
// Ensure that data-align attribute is set by default when media is inserted
|
||||
// while filter_align is enabled.
|
||||
FilterFormat::load('test_format')
|
||||
->setFilterConfig('filter_align', ['status' => TRUE])
|
||||
->save();
|
||||
$this->drupalGet('/node/add/blog');
|
||||
$this->waitForEditor();
|
||||
$this->pressEditorButton('Insert Media');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-modal #media-library-content'));
|
||||
$assert_session->elementExists('css', '.js-media-library-item')->click();
|
||||
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', $media_preview_selector, 1000));
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$drupal_media = $xpath->query('//drupal-media')[0];
|
||||
$expected_attributes = [
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => $this->media->uuid(),
|
||||
];
|
||||
foreach ($expected_attributes as $name => $expected) {
|
||||
$this->assertSame($expected, $drupal_media->getAttribute($name));
|
||||
}
|
||||
// Ensure that by default, data-align attribute is not set.
|
||||
$this->assertFalse($drupal_media->hasAttribute('data-align'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the allowed media types setting on the MediaEmbed filter.
|
||||
*/
|
||||
public function testAllowedMediaTypes(): void {
|
||||
$test_cases = [
|
||||
'all_media_types' => [],
|
||||
'only_image' => ['image' => 'image'],
|
||||
'only_arrakis' => ['arrakis' => 'arrakis'],
|
||||
'both_items_checked' => [
|
||||
'image' => 'image',
|
||||
'arrakis' => 'arrakis',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($test_cases as $allowed_media_types) {
|
||||
// Update the filter format to set the allowed media types.
|
||||
FilterFormat::load('test_format')
|
||||
->setFilterConfig('media_embed', [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_media_types' => $allowed_media_types,
|
||||
],
|
||||
])->save();
|
||||
|
||||
// Now test opening the media library from the CKEditor plugin, and
|
||||
// verify the expected behavior.
|
||||
$this->drupalGet('/node/add/blog');
|
||||
$this->waitForEditor();
|
||||
$this->pressEditorButton('Insert Media');
|
||||
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-modal #media-library-wrapper'));
|
||||
|
||||
if (empty($allowed_media_types) || count($allowed_media_types) === 2) {
|
||||
$menu = $assert_session->elementExists('css', '.js-media-library-menu');
|
||||
$assert_session->elementExists('named', ['link', 'Image'], $menu);
|
||||
$assert_session->elementExists('named', ['link', 'Arrakis'], $menu);
|
||||
$assert_session->elementTextContains('css', '.js-media-library-item', 'Fear is the mind-killer');
|
||||
}
|
||||
elseif (count($allowed_media_types) === 1 && !empty($allowed_media_types['image'])) {
|
||||
// No tabs should appear if there's only one media type available.
|
||||
$assert_session->elementNotExists('css', '.js-media-library-menu');
|
||||
$assert_session->elementTextContains('css', '.js-media-library-item', 'Fear is the mind-killer');
|
||||
}
|
||||
elseif (count($allowed_media_types) === 1 && !empty($allowed_media_types['arrakis'])) {
|
||||
// No tabs should appear if there's only one media type available.
|
||||
$assert_session->elementNotExists('css', '.js-media-library-menu');
|
||||
$assert_session->elementTextContains('css', '.js-media-library-item', 'Le baron Vladimir Harkonnen');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that alt text can be changed on Media Library inserted Media.
|
||||
*/
|
||||
public function testAlt(): void {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->drupalGet('/node/add/blog');
|
||||
$this->waitForEditor();
|
||||
$this->pressEditorButton('Insert Media');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-modal #media-library-content'));
|
||||
$assert_session->elementExists('css', '.js-media-library-item')->click();
|
||||
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media img'));
|
||||
|
||||
// Test that clicking the media widget triggers a CKEditor balloon panel
|
||||
// with a single button to override the alt text.
|
||||
$this->click('.ck-widget.drupal-media');
|
||||
$this->assertVisibleBalloon('[aria-label="Drupal Media toolbar"]');
|
||||
// Click the "Override media image text alternative" button.
|
||||
$this->getBalloonButton('Override media image alternative text')->click();
|
||||
$this->assertVisibleBalloon('.ck-media-alternative-text-form');
|
||||
// Assert that the value is currently empty.
|
||||
$alt_override_input = $page->find('css', '.ck-balloon-panel .ck-media-alternative-text-form input[type=text]');
|
||||
$this->assertSame('', $alt_override_input->getValue());
|
||||
|
||||
$test_alt = 'Alt text override';
|
||||
$alt_override_input->setValue($test_alt);
|
||||
$this->getBalloonButton('Save')->click();
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ck-widget.drupal-media img[alt*="' . $test_alt . '"]'));
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$drupal_media = $xpath->query('//drupal-media')[0];
|
||||
$this->assertEquals($test_alt, $drupal_media->getAttribute('alt'));
|
||||
|
||||
// Test that the media item can be designated 'decorative'.
|
||||
// Click the "Override media image text alternative" button.
|
||||
$this->getBalloonButton('Override media image alternative text')->click();
|
||||
$page->pressButton('Decorative image');
|
||||
$this->getBalloonButton('Save')->click();
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$drupal_media = $xpath->query('//drupal-media')[0];
|
||||
// The alt text in CKEditor displays alt="""", indicating
|
||||
// decorative image (https://www.w3.org/WAI/tutorials/images/decorative/).
|
||||
$this->assertEquals('""', $drupal_media->getAttribute('alt'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,310 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class MediaLinkabilityTest extends MediaTestBase {
|
||||
|
||||
/**
|
||||
* Ensures arbitrary attributes can be added on links wrapping media via GHS.
|
||||
*
|
||||
* @dataProvider providerLinkability
|
||||
*/
|
||||
public function testLinkedMediaArbitraryHtml(bool $unrestricted): void {
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$editor = Editor::load('test_format');
|
||||
$settings = $editor->getSettings();
|
||||
$filter_format = $editor->getFilterFormat();
|
||||
if ($unrestricted) {
|
||||
$filter_format
|
||||
->setFilterConfig('filter_html', ['status' => FALSE]);
|
||||
}
|
||||
else {
|
||||
// Allow the data-foo attribute in <a> via GHS. Also, add support for
|
||||
// div's with data-foo attribute to ensure that linked drupal-media
|
||||
// elements can be wrapped with <div>.
|
||||
$settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = ['<a data-foo>', '<div data-bar>'];
|
||||
$editor->setSettings($settings);
|
||||
$filter_format->setFilterConfig('filter_html', [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <br> <strong> <em> <a href data-foo> <drupal-media data-entity-type data-entity-uuid data-align data-caption alt data-view-mode> <div data-bar>',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$editor->save();
|
||||
$filter_format->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
|
||||
// Wrap the existing drupal-media tag with a div and an a that include
|
||||
// attributes allowed via GHS.
|
||||
$original_value = $this->host->body->value;
|
||||
$this->host->body->value = '<div data-bar="baz"><a href="https://example.com" data-foo="bar">' . $original_value . '</a></div>';
|
||||
$this->host->save();
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
|
||||
// Confirm data-foo is present in the editing view.
|
||||
$this->assertNotEmpty($link = $assert_session->waitForElementVisible('css', 'a[href="https://example.com"]'));
|
||||
$this->assertEquals('bar', $link->getAttribute('data-foo'));
|
||||
|
||||
// Confirm that the media is wrapped by the div on the editing view.
|
||||
$assert_session->elementExists('css', 'div[data-bar="baz"] > .drupal-media > a[href="https://example.com"] > div[data-drupal-media-preview]');
|
||||
|
||||
// Confirm that drupal-media is wrapped by the div and a, and that GHS has
|
||||
// retained arbitrary HTML allowed by source editing.
|
||||
$editor_dom = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($editor_dom->query('//div[@data-bar="baz"]/a[@data-foo="bar"]/drupal-media'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests linkability of the media CKEditor widget.
|
||||
*
|
||||
* Due to the very different HTML markup generated for the editing view and
|
||||
* the data view, this is explicitly testing the "editingDowncast" and
|
||||
* "dataDowncast" results. These are CKEditor 5 concepts.
|
||||
*
|
||||
* @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#conversion
|
||||
*
|
||||
* @dataProvider providerLinkability
|
||||
*/
|
||||
public function testLinkability(bool $unrestricted): void {
|
||||
// Disable filter_html.
|
||||
if ($unrestricted) {
|
||||
FilterFormat::load('test_format')
|
||||
->setFilterConfig('filter_html', ['status' => FALSE])
|
||||
->save();
|
||||
}
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Initial state: the Drupal Media CKEditor Widget is not selected.
|
||||
$drupalmedia = $assert_session->waitForElementVisible('css', '.ck-content .ck-widget.drupal-media');
|
||||
$this->assertNotEmpty($drupalmedia);
|
||||
$this->assertFalse($drupalmedia->hasClass('.ck-widget_selected'));
|
||||
|
||||
// Assert the "editingDowncast" HTML before making changes.
|
||||
$assert_session->elementExists('css', '.ck-content .ck-widget.drupal-media > [data-drupal-media-preview]');
|
||||
|
||||
// Assert the "dataDowncast" HTML before making changes.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query('//drupal-media'));
|
||||
$this->assertEmpty($xpath->query('//a'));
|
||||
|
||||
// Assert the link button is present and not pressed.
|
||||
$link_button = $this->getEditorButton('Link');
|
||||
$this->assertSame('false', $link_button->getAttribute('aria-pressed'));
|
||||
|
||||
// Wait for the preview to load.
|
||||
$preview = $assert_session->waitForElement('css', '.ck-content .ck-widget.drupal-media [data-drupal-media-preview="ready"]');
|
||||
$this->assertNotEmpty($preview);
|
||||
|
||||
// Tests linking Drupal media.
|
||||
$drupalmedia->click();
|
||||
$this->assertTrue($drupalmedia->hasClass('ck-widget_selected'));
|
||||
$this->assertEditorButtonEnabled('Link');
|
||||
// Assert structure of image toolbar balloon.
|
||||
$this->assertVisibleBalloon('.ck-toolbar[aria-label="Drupal Media toolbar"]');
|
||||
$link_media_button = $this->getBalloonButton('Link media');
|
||||
// Click the "Link media" button.
|
||||
$this->assertSame('false', $link_media_button->getAttribute('aria-pressed'));
|
||||
$link_media_button->press();
|
||||
// Assert structure of link form balloon.
|
||||
$balloon = $this->assertVisibleBalloon('.ck-link-form');
|
||||
$url_input = $balloon->find('css', '.ck-labeled-field-view__input-wrapper .ck-input-text[inputmode=url]');
|
||||
// Fill in link form balloon's <input> and hit "Insert".
|
||||
$url_input->setValue('http://linking-embedded-media.com');
|
||||
$balloon->pressButton('Insert');
|
||||
|
||||
// Assert the "editingDowncast" HTML after making changes. Assert the link
|
||||
// exists, then assert the link exists. Then assert the expected DOM
|
||||
// structure in detail.
|
||||
$assert_session->elementExists('css', '.ck-content a[href="http://linking-embedded-media.com"]');
|
||||
$assert_session->elementExists('css', '.ck-content .drupal-media.ck-widget > a[href="http://linking-embedded-media.com"] > div[aria-label] > article > div > img[src*="image-test.png"]');
|
||||
|
||||
// Assert the "dataDowncast" HTML after making changes.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query('//drupal-media'));
|
||||
$this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]'));
|
||||
$this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]/drupal-media'));
|
||||
// Ensure that the media caption is retained and not linked as a result of
|
||||
// linking media.
|
||||
$this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]/drupal-media[@data-caption="baz"]'));
|
||||
|
||||
// Add `class="trusted"` to the link.
|
||||
$this->assertEmpty($xpath->query('//a[@href="http://linking-embedded-media.com" and @class="trusted"]'));
|
||||
$this->pressEditorButton('Source');
|
||||
$source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea');
|
||||
$this->assertNotEmpty($source_text_area);
|
||||
$new_value = str_replace('<a ', '<a class="trusted" ', $source_text_area->getValue());
|
||||
$source_text_area->setValue('<p>temp</p>');
|
||||
$source_text_area->setValue($new_value);
|
||||
$this->pressEditorButton('Source');
|
||||
|
||||
// When unrestricted, additional attributes on links should be retained.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertCount($unrestricted ? 1 : 0, $xpath->query('//a[@href="http://linking-embedded-media.com" and @class="trusted"]'));
|
||||
|
||||
// Save the entity whose text field is being edited.
|
||||
$page->pressButton('Save');
|
||||
|
||||
// Assert the HTML the end user sees.
|
||||
$assert_session->elementExists('css', $unrestricted
|
||||
? 'a[href="http://linking-embedded-media.com"].trusted img[src*="image-test.png"]'
|
||||
: 'a[href="http://linking-embedded-media.com"] img[src*="image-test.png"]');
|
||||
|
||||
// Go back to edit the now *linked* <drupal-media>. Everything from this
|
||||
// point onwards is effectively testing "upcasting" and proving there is no
|
||||
// data loss.
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
// Assert the "dataDowncast" HTML before making changes.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query('//drupal-media'));
|
||||
$this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]'));
|
||||
$this->assertNotEmpty($xpath->query('//a[@href="http://linking-embedded-media.com"]/drupal-media'));
|
||||
|
||||
// Tests unlinking media.
|
||||
$drupalmedia->click();
|
||||
$this->assertEditorButtonEnabled('Link');
|
||||
$this->assertSame('true', $this->getEditorButton('Link')->getAttribute('aria-pressed'));
|
||||
// Assert structure of Drupal media toolbar balloon.
|
||||
$this->assertVisibleBalloon('.ck-toolbar[aria-label="Drupal Media toolbar"]');
|
||||
$link_media_button = $this->getBalloonButton('Link media');
|
||||
$this->assertSame('true', $link_media_button->getAttribute('aria-pressed'));
|
||||
$link_media_button->click();
|
||||
// Assert structure of link actions balloon.
|
||||
$this->getBalloonButton('Edit link');
|
||||
$unlink_image_button = $this->getBalloonButton('Unlink');
|
||||
// Click the "Unlink" button.
|
||||
$unlink_image_button->click();
|
||||
$this->assertSame('false', $this->getEditorButton('Link')->getAttribute('aria-pressed'));
|
||||
|
||||
// Assert the "editingDowncast" HTML after making changes. Assert the link
|
||||
// exists, then assert no link exists. Then assert the expected DOM
|
||||
// structure in detail.
|
||||
$assert_session->elementNotExists('css', '.ck-content a');
|
||||
$assert_session->elementExists('css', '.ck-content .drupal-media.ck-widget > div[aria-label] > article > div > img[src*="image-test.png"]');
|
||||
|
||||
// Ensure that figcaption exists.
|
||||
// @see https://www.drupal.org/project/drupal/issues/3268318
|
||||
$assert_session->elementExists('css', '.ck-content .drupal-media.ck-widget > figcaption');
|
||||
|
||||
// Assert the "dataDowncast" HTML after making changes.
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query('//drupal-media'));
|
||||
$this->assertEmpty($xpath->query('//a'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data for multiple tests.
|
||||
*
|
||||
* Provides data for testLinkability(), testLinkManualDecorator() and
|
||||
* testLinkedMediaArbitraryHtml().
|
||||
*/
|
||||
public static function providerLinkability(): array {
|
||||
return [
|
||||
'restricted' => [FALSE],
|
||||
'unrestricted' => [TRUE],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that manual link decorators work with linkable media.
|
||||
*
|
||||
* @dataProvider providerLinkability
|
||||
*/
|
||||
public function testLinkManualDecorator(bool $unrestricted): void {
|
||||
\Drupal::service('module_installer')->install(['ckeditor5_manual_decorator_test']);
|
||||
$this->resetAll();
|
||||
|
||||
$decorator = 'Open in a new tab';
|
||||
$decorator_attributes = '[@target="_blank"][@rel="noopener noreferrer"][@class="link-new-tab"]';
|
||||
|
||||
// Disable filter_html.
|
||||
if ($unrestricted) {
|
||||
FilterFormat::load('test_format')
|
||||
->setFilterConfig('filter_html', ['status' => FALSE])
|
||||
->save();
|
||||
$decorator = 'Pink color';
|
||||
$decorator_attributes = '[@style="color:pink;"]';
|
||||
}
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->assertNotEmpty($drupalmedia = $assert_session->waitForElementVisible('css', '.ck-content .ck-widget.drupal-media'));
|
||||
$drupalmedia->click();
|
||||
$this->assertVisibleBalloon('.ck-toolbar[aria-label="Drupal Media toolbar"]');
|
||||
|
||||
// Turn off caption, so we don't accidentally put our link in that text
|
||||
// field instead of on the actual media.
|
||||
$this->getBalloonButton('Toggle caption off')->click();
|
||||
$assert_session->assertNoElementAfterWait('css', 'figure.drupal-media > figcaption');
|
||||
|
||||
$this->assertVisibleBalloon('.ck-toolbar[aria-label="Drupal Media toolbar"]');
|
||||
$this->getBalloonButton('Link media')->click();
|
||||
|
||||
$balloon = $this->assertVisibleBalloon('.ck-link-form');
|
||||
$url_input = $balloon->find('css', '.ck-labeled-field-view__input-wrapper .ck-input-text[inputmode=url]');
|
||||
$url_input->setValue('http://linking-embedded-media.com');
|
||||
$balloon->pressButton('Insert');
|
||||
$this->getBalloonButton('Link properties')->click();
|
||||
$this->getBalloonButton($decorator)->click();
|
||||
$this->getBalloonButton('Back')->click();
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.drupal-media a'));
|
||||
$this->assertVisibleBalloon('.ck-link-toolbar');
|
||||
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query("//a[@href='http://linking-embedded-media.com']$decorator_attributes"));
|
||||
$this->assertNotEmpty($xpath->query("//a[@href='http://linking-embedded-media.com']$decorator_attributes/drupal-media"));
|
||||
|
||||
// Ensure that manual decorators upcast correctly.
|
||||
$page->pressButton('Save');
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->assertNotEmpty($drupalmedia = $assert_session->waitForElementVisible('css', '.ck-content .ck-widget.drupal-media'));
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertNotEmpty($xpath->query("//a[@href='http://linking-embedded-media.com']$decorator_attributes"));
|
||||
$this->assertNotEmpty($xpath->query("//a[@href='http://linking-embedded-media.com']$decorator_attributes/drupal-media"));
|
||||
|
||||
// Finally, ensure that media can be unlinked.
|
||||
$drupalmedia->click();
|
||||
$this->assertVisibleBalloon('.ck-toolbar[aria-label="Drupal Media toolbar"]');
|
||||
$this->getBalloonButton('Link media')->click();
|
||||
$this->assertVisibleBalloon('.ck-link-toolbar');
|
||||
$this->getBalloonButton('Unlink')->click();
|
||||
|
||||
$this->assertTrue($assert_session->waitForElementRemoved('css', '.drupal-media a'));
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertEmpty($xpath->query('//a'));
|
||||
$this->assertNotEmpty($xpath->query('//drupal-media'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
// cspell:ignore drupalmediaediting
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Media
|
||||
* @group ckeditor5
|
||||
* @group #slow
|
||||
* @internal
|
||||
*/
|
||||
class MediaPreviewTest extends MediaTestBase {
|
||||
|
||||
/**
|
||||
* Tests that failed media embed preview requests inform the end user.
|
||||
*/
|
||||
public function testErrorMessages(): void {
|
||||
// This test currently frequently causes the SQLite database to lock, so
|
||||
// skip the test on SQLite until the issue can be resolved.
|
||||
// @todo https://www.drupal.org/project/drupal/issues/3273626
|
||||
if (Database::getConnection()->driver() === 'sqlite') {
|
||||
$this->markTestSkipped('Test frequently causes a locked database on SQLite');
|
||||
}
|
||||
|
||||
// Assert that a request to the `media.filter.preview` route that does not
|
||||
// result in a 200 response (due to server error or network error) is
|
||||
// handled in the JavaScript by displaying the expected error message.
|
||||
// @see core/modules/media/js/media_embed_ckeditor.theme.js
|
||||
// @see js/ckeditor5_plugins/drupalMedia/src/drupalmediaediting.js
|
||||
$this->container->get('state')->set('test_media_filter_controller_throw_error', TRUE);
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$assert_session = $this->assertSession();
|
||||
$assert_session->waitForElementVisible('css', '.ck-widget.drupal-media');
|
||||
$this->assertEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]', 1000));
|
||||
$assert_session->elementNotExists('css', '.ck-widget.drupal-media .media');
|
||||
$this->assertNotEmpty($assert_session->waitForText('An error occurred while trying to preview the media. Save your work and reload this page.'));
|
||||
// Now assert that the error doesn't appear when the override to force an
|
||||
// error is removed.
|
||||
$this->container->get('state')->set('test_media_filter_controller_throw_error', FALSE);
|
||||
$this->getSession()->reload();
|
||||
$this->waitForEditor();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
|
||||
|
||||
// There's a second kind of error message that comes from the back end
|
||||
// that happens when the media uuid can't be converted to a media preview.
|
||||
// In this case, the error will appear in a the themeable
|
||||
// media-embed-error.html template. We have a hook altering the css
|
||||
// classes to test the twig template is working properly and picking up our
|
||||
// extra class.
|
||||
// @see \Drupal\media\Plugin\Filter\MediaEmbed::renderMissingMediaIndicator()
|
||||
// @see core/modules/media/templates/media-embed-error.html.twig
|
||||
// @see media_test_embed_preprocess_media_embed_error()
|
||||
$original_value = $this->host->body->value;
|
||||
$this->host->body->value = str_replace($this->media->uuid(), 'invalid_uuid', $original_value);
|
||||
$this->host->save();
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-widget.drupal-media .this-error-message-is-themeable'));
|
||||
|
||||
// Test when using the starterkit_theme theme, an additional class is added
|
||||
// to the error, which is supported by
|
||||
// stable9/templates/content/media-embed-error.html.twig.
|
||||
$this->assertTrue($this->container->get('theme_installer')->install(['starterkit_theme']));
|
||||
$this->config('system.theme')
|
||||
->set('default', 'starterkit_theme')
|
||||
->save();
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-widget.drupal-media .this-error-message-is-themeable'));
|
||||
|
||||
// Test that restoring a valid UUID results in the media embed preview
|
||||
// displaying.
|
||||
$this->host->body->value = $original_value;
|
||||
$this->host->save();
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
|
||||
$assert_session->elementNotExists('css', '.ck-widget.drupal-media .this-error-message-is-themeable');
|
||||
}
|
||||
|
||||
/**
|
||||
* The CKEditor Widget must load a preview generated using the default theme.
|
||||
*/
|
||||
public function testPreviewUsesDefaultThemeAndIsClientCacheable(): void {
|
||||
// Allow the test user to view the admin theme.
|
||||
$this->adminUser
|
||||
->addRole($this->drupalCreateRole(['view the administration theme']))
|
||||
->save();
|
||||
|
||||
// Configure a different default and admin theme, like on most Drupal sites.
|
||||
$this->config('system.theme')
|
||||
->set('default', 'stable9')
|
||||
->set('admin', 'starterkit_theme')
|
||||
->save();
|
||||
|
||||
// Assert that when looking at an embedded entity in the CKEditor Widget,
|
||||
// the preview is generated using the default theme, not the admin theme.
|
||||
// @see media_test_embed_entity_view_alter()
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
|
||||
$element = $assert_session->elementExists('css', '[data-media-embed-test-active-theme]');
|
||||
$this->assertSame('stable9', $element->getAttribute('data-media-embed-test-active-theme'));
|
||||
// Assert that the first preview request transferred >500 B over the wire.
|
||||
// Then toggle source mode on and off. This causes the CKEditor widget to be
|
||||
// destroyed and then reconstructed. Assert that during this reconstruction,
|
||||
// a second request is sent. This second request should have transferred 0
|
||||
// bytes: the browser should have cached the response, thus resulting in a
|
||||
// much better user experience.
|
||||
$this->assertGreaterThan(500, $this->getLastPreviewRequestTransferSize());
|
||||
$this->pressEditorButton('Source');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-source-editing-area'));
|
||||
// CKEditor 5 is very smart: if no changes were made in the Source Editing
|
||||
// Area, it will not rerender the contents. In this test, we
|
||||
// want to verify that Media preview responses are cached on the client side
|
||||
// so it is essential that rerendering occurs. To achieve this, we append a
|
||||
// single space.
|
||||
$source_text_area = $this->getSession()->getPage()->find('css', '[name="body[0][value]"] + .ck-editor textarea');
|
||||
$source_text_area->setValue($source_text_area->getValue() . ' ');
|
||||
$this->pressEditorButton('Source');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
|
||||
$this->assertSame(0, $this->getLastPreviewRequestTransferSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests preview route access.
|
||||
*
|
||||
* @param bool $media_embed_enabled
|
||||
* Whether to test with media_embed filter enabled on the text format.
|
||||
* @param bool $can_use_format
|
||||
* Whether the logged in user is allowed to use the text format.
|
||||
*
|
||||
* @dataProvider previewAccessProvider
|
||||
*/
|
||||
public function testEmbedPreviewAccess($media_embed_enabled, $can_use_format): void {
|
||||
// Reconfigure the host entity's text format to suit our needs.
|
||||
/** @var \Drupal\filter\FilterFormatInterface $format */
|
||||
$format = FilterFormat::load($this->host->body->format);
|
||||
$format->set('filters', [
|
||||
'filter_align' => ['status' => TRUE],
|
||||
'filter_caption' => ['status' => TRUE],
|
||||
'media_embed' => ['status' => $media_embed_enabled],
|
||||
]);
|
||||
$format->save();
|
||||
|
||||
$permissions = [
|
||||
'bypass node access',
|
||||
];
|
||||
if ($can_use_format) {
|
||||
$permissions[] = $format->getPermissionName();
|
||||
}
|
||||
$this->drupalLogin($this->drupalCreateUser($permissions));
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
|
||||
$assert_session = $this->assertSession();
|
||||
if ($can_use_format) {
|
||||
$this->waitForEditor();
|
||||
if ($media_embed_enabled) {
|
||||
// The preview rendering, which in this test will use Starterkit theme's
|
||||
// media.html.twig template, will fail without the CSRF token/header.
|
||||
// @see ::testEmbeddedMediaPreviewWithCsrfToken()
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'article.media'));
|
||||
}
|
||||
else {
|
||||
// If the filter isn't enabled, there won't be an error, but the
|
||||
// preview shouldn't be rendered.
|
||||
$assert_session->elementNotExists('css', 'article.media');
|
||||
}
|
||||
}
|
||||
else {
|
||||
$assert_session->pageTextContains('This field has been disabled because you do not have sufficient permissions to edit it.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testEmbedPreviewAccess.
|
||||
*/
|
||||
public static function previewAccessProvider() {
|
||||
return [
|
||||
'media_embed filter enabled' => [
|
||||
TRUE,
|
||||
TRUE,
|
||||
],
|
||||
'media_embed filter disabled' => [
|
||||
FALSE,
|
||||
TRUE,
|
||||
],
|
||||
'media_embed filter enabled, user not allowed to use text format' => [
|
||||
TRUE,
|
||||
FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure media preview isn't clickable.
|
||||
*/
|
||||
public function testMediaPointerEvent(): void {
|
||||
$entityViewDisplay = EntityViewDisplay::load('media.image.view_mode_1');
|
||||
$thumbnail = $entityViewDisplay->getComponent('thumbnail');
|
||||
$thumbnail['settings']['image_link'] = 'file';
|
||||
$entityViewDisplay->setComponent('thumbnail', $thumbnail);
|
||||
$entityViewDisplay->save();
|
||||
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
$url = $this->host->toUrl('edit-form');
|
||||
$this->drupalGet($url);
|
||||
$this->waitForEditor();
|
||||
$assert_session->waitForLink('default alt');
|
||||
$page->find('css', '.ck .drupal-media')->click();
|
||||
// Assert that the media preview is not clickable by comparing the URL.
|
||||
$this->assertEquals($url->toString(), $this->getUrl());
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
|
||||
use Drupal\Core\Entity\Entity\EntityViewMode;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\media\Entity\Media;
|
||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* Base class for CKEditor 5 Media integration tests.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class MediaTestBase extends WebDriverTestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
use MediaTypeCreationTrait;
|
||||
use TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* The user to use during testing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* The sample Media entity to embed.
|
||||
*
|
||||
* @var \Drupal\media\MediaInterface
|
||||
*/
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* The second sample Media entity to embed used in one of the tests.
|
||||
*
|
||||
* @var \Drupal\media\MediaInterface
|
||||
*/
|
||||
protected $mediaFile;
|
||||
|
||||
/**
|
||||
* A host entity with a body field to embed media in.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
'media',
|
||||
'node',
|
||||
'text',
|
||||
'media_test_embed',
|
||||
'media_library',
|
||||
'ckeditor5_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'starterkit_theme';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
EntityViewMode::create([
|
||||
'id' => 'media.view_mode_1',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'enabled' => TRUE,
|
||||
'label' => 'View Mode 1',
|
||||
])->save();
|
||||
EntityViewMode::create([
|
||||
'id' => 'media.22222',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'enabled' => TRUE,
|
||||
'label' => 'View Mode 2 has Numeric ID',
|
||||
])->save();
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <br> <strong> <em> <a href> <drupal-media data-entity-type data-entity-uuid data-align data-view-mode data-caption alt>',
|
||||
],
|
||||
],
|
||||
'filter_align' => ['status' => TRUE],
|
||||
'filter_caption' => ['status' => TRUE],
|
||||
'media_embed' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'default_view_mode' => 'view_mode_1',
|
||||
'allowed_view_modes' => [
|
||||
'view_mode_1' => 'view_mode_1',
|
||||
'22222' => '22222',
|
||||
],
|
||||
'allowed_media_types' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'sourceEditing',
|
||||
'link',
|
||||
'bold',
|
||||
'italic',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
'media_media' => [
|
||||
'allow_view_mode_override' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
|
||||
// Note that media_install() grants 'view media' to all users by default.
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
]);
|
||||
|
||||
// Create a sample media entity to be embedded.
|
||||
$this->createMediaType('image', ['id' => 'image']);
|
||||
File::create([
|
||||
'uri' => $this->getTestFiles('image')[0]->uri,
|
||||
])->save();
|
||||
$this->media = Media::create([
|
||||
'bundle' => 'image',
|
||||
'name' => 'Screaming hairy armadillo',
|
||||
'field_media_image' => [
|
||||
[
|
||||
'target_id' => 1,
|
||||
'alt' => 'default alt',
|
||||
'title' => 'default title',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->media->save();
|
||||
|
||||
$this->createMediaType('file', ['id' => 'file']);
|
||||
File::create([
|
||||
'uri' => $this->getTestFiles('text')[0]->uri,
|
||||
])->save();
|
||||
$this->mediaFile = Media::create([
|
||||
'bundle' => 'file',
|
||||
'name' => 'Information about screaming hairy armadillo',
|
||||
'field_media_file' => [
|
||||
[
|
||||
'target_id' => 2,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->mediaFile->save();
|
||||
|
||||
// Set created media types for each view mode.
|
||||
EntityViewDisplay::create([
|
||||
'id' => 'media.image.view_mode_1',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'bundle' => 'image',
|
||||
'mode' => 'view_mode_1',
|
||||
])->save();
|
||||
EntityViewDisplay::create([
|
||||
'id' => 'media.image.22222',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'bundle' => 'image',
|
||||
'mode' => '22222',
|
||||
])->save();
|
||||
|
||||
// Create a sample host entity to embed media in.
|
||||
$this->drupalCreateContentType(['type' => 'blog']);
|
||||
$this->host = $this->createNode([
|
||||
'type' => 'blog',
|
||||
'title' => 'Animals with strange names',
|
||||
'body' => [
|
||||
'value' => '<drupal-media data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '" data-caption="baz"></drupal-media>',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies value of an attribute on the downcast <drupal-media> element.
|
||||
*
|
||||
* Assumes CKEditor is in source mode.
|
||||
*
|
||||
* @param string $attribute
|
||||
* The attribute to check.
|
||||
* @param string|null $value
|
||||
* Either a string value or if NULL, asserts that <drupal-media> element
|
||||
* doesn't have the attribute.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function assertSourceAttributeSame(string $attribute, ?string $value): void {
|
||||
$dom = $this->getEditorDataAsDom();
|
||||
$drupal_media = (new \DOMXPath($dom))->query('//drupal-media');
|
||||
$this->assertNotEmpty($drupal_media);
|
||||
if ($value === NULL) {
|
||||
$this->assertFalse($drupal_media[0]->hasAttribute($attribute));
|
||||
}
|
||||
else {
|
||||
$this->assertSame($value, $drupal_media[0]->getAttribute($attribute));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transfer size of the last preview request.
|
||||
*
|
||||
* @return int
|
||||
* The size of the bytes transferred.
|
||||
*/
|
||||
protected function getLastPreviewRequestTransferSize(): int {
|
||||
$javascript = <<<JS
|
||||
(function(){
|
||||
return window.performance
|
||||
.getEntries()
|
||||
.filter(function (entry) {
|
||||
return entry.initiatorType == 'fetch' && entry.name.indexOf('/media/test_format/preview') !== -1;
|
||||
})
|
||||
.pop()
|
||||
.transferSize;
|
||||
})()
|
||||
JS;
|
||||
return $this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\HTMLRestrictions;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
// cspell:ignore sourceediting
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing
|
||||
* @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class SourceEditingEmptyElementTest extends SourceEditingTestBase {
|
||||
|
||||
/**
|
||||
* Tests creating empty inline elements using Source Editing.
|
||||
*
|
||||
* @testWith ["<p>Before <i class=\"fab fa-drupal\"></i> and after.</p>", "<p>Before and after.</p>", "<p>Before and after.</p>", null]
|
||||
* ["<p>Before <i class=\"fab fa-drupal\"></i> and after.</p>", "<p>Before and after.</p>", null, "<i>"]
|
||||
* ["<p>Before <i class=\"fab fa-drupal\"></i> and after.</p>", null, null, "<i class>"]
|
||||
* ["<p>Before <span class=\"icon my-icon\"></span> and after.</p>", "<p>Before and after.</p>", "<p>Before and after.</p>", null]
|
||||
* ["<p>Before <span class=\"icon my-icon\"></span> and after.</p>", "<p>Before and after.</p>", null, "<span>"]
|
||||
* ["<p>Before <span class=\"icon my-icon\"></span> and after.</p>", "<p>Before <span class=\"icon\"></span> and after.</p>", null, "<span class=\"icon\">"]
|
||||
*/
|
||||
public function testEmptyInlineElement(string $input, ?string $expected_output_when_restricted, ?string $expected_output_when_unrestricted, ?string $allowed_elements_string): void {
|
||||
$this->host->body->value = $input;
|
||||
$this->host->save();
|
||||
|
||||
// If no expected output is specified, it should be identical to the input.
|
||||
if ($expected_output_when_restricted === NULL) {
|
||||
$expected_output_when_restricted = $input;
|
||||
}
|
||||
if ($expected_output_when_unrestricted === NULL) {
|
||||
$expected_output_when_unrestricted = $input;
|
||||
}
|
||||
|
||||
$text_editor = Editor::load('test_format');
|
||||
$text_format = FilterFormat::load('test_format');
|
||||
if ($allowed_elements_string) {
|
||||
// Allow creating additional HTML using SourceEditing.
|
||||
$settings = $text_editor->getSettings();
|
||||
$settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'][] = $allowed_elements_string;
|
||||
$text_editor->setSettings($settings);
|
||||
|
||||
// Keep the allowed HTML tags in sync.
|
||||
$allowed_elements = HTMLRestrictions::fromTextFormat($text_format);
|
||||
$updated_allowed_tags = $allowed_elements->merge(HTMLRestrictions::fromString($allowed_elements_string));
|
||||
$filter_html_config = $text_format->filters('filter_html')
|
||||
->getConfiguration();
|
||||
$filter_html_config['settings']['allowed_html'] = $updated_allowed_tags->toFilterHtmlAllowedTagsString();
|
||||
$text_format->setFilterConfig('filter_html', $filter_html_config);
|
||||
|
||||
// Verify the text format and editor are still a valid pair.
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
$text_editor,
|
||||
$text_format
|
||||
))
|
||||
));
|
||||
|
||||
// If valid, save both.
|
||||
$text_format->save();
|
||||
$text_editor->save();
|
||||
}
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assertSame($expected_output_when_restricted, $this->getEditorDataAsHtmlString());
|
||||
|
||||
// Make the text format unrestricted: disable filter_html.
|
||||
$text_format
|
||||
->setFilterConfig('filter_html', ['status' => FALSE])
|
||||
->save();
|
||||
|
||||
// Verify the text format and editor are still a valid pair.
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
$text_editor,
|
||||
$text_format
|
||||
))
|
||||
));
|
||||
|
||||
// Test with a text format allowing arbitrary HTML.
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assertSame($expected_output_when_unrestricted, $this->getEditorDataAsHtmlString());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\HTMLRestrictions;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
// cspell:ignore gramma sourceediting
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing
|
||||
* @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class SourceEditingTest extends SourceEditingTestBase {
|
||||
|
||||
/**
|
||||
* @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing::buildConfigurationForm
|
||||
*/
|
||||
public function testSourceEditingSettingsForm(): void {
|
||||
$this->drupalLogin($this->drupalCreateUser(['administer filters']));
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
// The Source Editing plugin settings form should not be present.
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting"]');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-sourceEditing'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The Source Editing plugin settings form should now be present and should
|
||||
// have no allowed tags configured.
|
||||
$page->clickLink('Source editing');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]'));
|
||||
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]');
|
||||
allowedTags.value = '<div data-foo>';
|
||||
allowedTags.dispatchEvent(new Event('input'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
|
||||
// Immediately save the configuration. Intentionally do nothing that would
|
||||
// trigger an AJAX rebuild.
|
||||
$page->pressButton('Save configuration');
|
||||
|
||||
// Verify that the configuration was saved.
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$page->clickLink('Source editing');
|
||||
$this->assertNotNull($ghs_textarea = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]'));
|
||||
|
||||
$ghs_string = '<div data-foo>';
|
||||
$this->assertSame($ghs_string, $ghs_textarea->getValue());
|
||||
$allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$this->assertStringContainsString($ghs_string, $allowed_html_field->getValue(), "$ghs_string not found in the allowed tags value of: {$allowed_html_field->getValue()}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests allowing extra attributes on already supported tags using GHS.
|
||||
*/
|
||||
public function testAllowingExtraAttributes(): void {
|
||||
$original_text_editor = Editor::load('test_format');
|
||||
$original_text_format = FilterFormat::load('test_format');
|
||||
$allowed_elements = HTMLRestrictions::fromTextFormat($original_text_format);
|
||||
$filter_html_config = $original_text_format->filters('filter_html')
|
||||
->getConfiguration();
|
||||
foreach ($this->providerAllowingExtraAttributes() as $data) {
|
||||
$text_editor = clone $original_text_editor;
|
||||
$text_format = clone $original_text_format;
|
||||
[$original_markup, $expected_markup, $allowed_elements_string] = $data;
|
||||
// Allow creating additional HTML using SourceEditing.
|
||||
$settings = $text_editor->getSettings();
|
||||
if ($allowed_elements_string) {
|
||||
$settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'][] = $allowed_elements_string;
|
||||
}
|
||||
$text_editor->setSettings($settings);
|
||||
|
||||
$new_config = $filter_html_config;
|
||||
if ($allowed_elements_string) {
|
||||
// Keep the allowed HTML tags in sync.
|
||||
$updated_allowed_tags = $allowed_elements->merge(HTMLRestrictions::fromString($allowed_elements_string));
|
||||
$new_config['settings']['allowed_html'] = $updated_allowed_tags->toFilterHtmlAllowedTagsString();
|
||||
}
|
||||
$text_format->setFilterConfig('filter_html', $new_config);
|
||||
|
||||
// Verify the text format and editor are still a valid pair.
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
$text_editor,
|
||||
$text_format
|
||||
))
|
||||
));
|
||||
|
||||
// If valid, save both.
|
||||
$text_format->save();
|
||||
$text_editor->save();
|
||||
$this->doTestAllowingExtraAttributes($original_markup, $expected_markup, $allowed_elements_string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests extra attributes with a specific data set.
|
||||
*/
|
||||
protected function doTestAllowingExtraAttributes(string $original_markup, string $expected_markup, string $allowed_elements_string): void {
|
||||
$this->host->body->value = $original_markup;
|
||||
$this->host->save();
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assertSame($expected_markup, $this->getEditorDataAsHtmlString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testAllowingExtraAttributes().
|
||||
*
|
||||
* @return array
|
||||
* The test cases.
|
||||
*/
|
||||
protected function providerAllowingExtraAttributes(): array {
|
||||
$general_test_case_markup = '<div class="llama" data-llama="🦙"><p data-llama="🦙">The <a href="https://example.com/pirate" class="button" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" class="use-ajax" data-grammar="adjective">irate</a>.</p></div>';
|
||||
return [
|
||||
'no extra attributes allowed' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'',
|
||||
],
|
||||
|
||||
// Common case: any attribute that is not `style` or `class`.
|
||||
'<a data-grammar="subject">' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<a data-grammar="subject">',
|
||||
],
|
||||
'<a data-grammar="adjective">' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
|
||||
'<a data-grammar="adjective">',
|
||||
],
|
||||
'<a data-grammar>' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
|
||||
'<a data-grammar>',
|
||||
],
|
||||
|
||||
// Edge case: `class`.
|
||||
'<a class="button">' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<a class="button">',
|
||||
],
|
||||
'<a class="use-ajax">' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<a class="use-ajax">',
|
||||
],
|
||||
'<a class>' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<a class>',
|
||||
],
|
||||
|
||||
// Edge case: $text-container wildcard with additional
|
||||
// attribute.
|
||||
'<$text-container data-llama>' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama" data-llama="🦙"><p data-llama="🦙">The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<$text-container data-llama>',
|
||||
],
|
||||
// Edge case: $text-container wildcard with stricter attribute
|
||||
// constrain.
|
||||
'<$text-container class="not-llama">' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<$text-container class="not-llama">',
|
||||
],
|
||||
|
||||
// Edge case: wildcard attribute names:
|
||||
// - prefix, f.e. `data-*`
|
||||
// - infix, f.e. `*gramma*`
|
||||
// - suffix, f.e. `*-grammar`
|
||||
'<a data-*>' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
|
||||
'<a data-*>',
|
||||
],
|
||||
'<a *gramma*>' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
|
||||
'<a *gramma*>',
|
||||
],
|
||||
'<a *-grammar>' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
|
||||
'<a *-grammar>',
|
||||
],
|
||||
|
||||
// Edge case: concrete attribute with wildcard class value.
|
||||
'<a class="use-*">' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<a class="use-*">',
|
||||
],
|
||||
|
||||
// Edge case: concrete attribute with wildcard attribute value.
|
||||
'<a data-grammar="sub*">' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<a data-grammar="sub*">',
|
||||
],
|
||||
|
||||
// Edge case: `data-*` with wildcard attribute value.
|
||||
'<a data-*="sub*">' => [
|
||||
$general_test_case_markup,
|
||||
'<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
|
||||
'<a data-*="sub*">',
|
||||
],
|
||||
|
||||
// Edge case: `style`.
|
||||
// @todo https://www.drupal.org/project/drupal/issues/3304832
|
||||
|
||||
// Edge case: `type` attribute on lists.
|
||||
// @todo Remove in https://www.drupal.org/project/drupal/issues/3274635.
|
||||
'no numberedList-related additions to the Source Editing configuration' => [
|
||||
'<ol type="A"><li>foo</li><li>bar</li></ol>',
|
||||
'<ol><li>foo</li><li>bar</li></ol>',
|
||||
'',
|
||||
],
|
||||
'<ol type>' => [
|
||||
'<ol type="A"><li>foo</li><li>bar</li></ol>',
|
||||
'<ol type="A"><li>foo</li><li>bar</li></ol>',
|
||||
'<ol type>',
|
||||
],
|
||||
'<ol type="A">' => [
|
||||
'<ol type="A"><li>foo</li><li>bar</li></ol>',
|
||||
'<ol type="A"><li>foo</li><li>bar</li></ol>',
|
||||
'<ol type="A">',
|
||||
],
|
||||
'no bulletedList-related additions to the Source Editing configuration' => [
|
||||
'<ul type="circle"><li>foo</li><li>bar</li></ul>',
|
||||
'<ul><li>foo</li><li>bar</li></ul>',
|
||||
'',
|
||||
],
|
||||
'<ul type>' => [
|
||||
'<ul type="circle"><li>foo</li><li>bar</li></ul>',
|
||||
'<ul type="circle"><li>foo</li><li>bar</li></ul>',
|
||||
'<ul type>',
|
||||
],
|
||||
'<ul type="circle">' => [
|
||||
'<ul type="circle"><li>foo</li><li>bar</li></ul>',
|
||||
'<ul type="circle"><li>foo</li><li>bar</li></ul>',
|
||||
'<ul type="circle">',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
// cspell:ignore sourceediting
|
||||
|
||||
/**
|
||||
* Provides a base class for testing the source editing function.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class SourceEditingTestBase extends CKEditor5TestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* The user to use during testing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* A host entity with a body field whose text to edit with CKEditor 5.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
'node',
|
||||
'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<div class> <p> <br> <a href> <ol> <ul> <li>',
|
||||
],
|
||||
],
|
||||
'filter_align' => ['status' => TRUE],
|
||||
'filter_caption' => ['status' => TRUE],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'sourceEditing',
|
||||
'link',
|
||||
'bulletedList',
|
||||
'numberedList',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => ['<div class>'],
|
||||
],
|
||||
'ckeditor5_list' => [
|
||||
'properties' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
]);
|
||||
|
||||
// Create a sample host entity to test CKEditor 5.
|
||||
$this->host = $this->createNode([
|
||||
'type' => 'page',
|
||||
'title' => 'Animals with strange names',
|
||||
'body' => [
|
||||
'value' => '',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,664 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
// cspell:ignore sourceediting
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class StyleTest extends CKEditor5TestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style::buildConfigurationForm
|
||||
*/
|
||||
public function testStyleSettingsForm(): void {
|
||||
$this->drupalLogin($this->drupalCreateUser(['administer filters']));
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
|
||||
// The Style plugin settings form should not be present.
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style"]');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-style'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-style', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// No validation error upon enabling the Style plugin.
|
||||
$this->assertNoRealtimeValidationErrors();
|
||||
$assert_session->pageTextContains('No styles configured');
|
||||
|
||||
// Still no validation error when configuring other functionality first.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-undo', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertNoRealtimeValidationErrors();
|
||||
|
||||
// The Style plugin settings form should now be present and should have no
|
||||
// styles configured.
|
||||
$page->clickLink('Style');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]'));
|
||||
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
allowedTags.value = 'p.foo.bar | Foobar paragraph';
|
||||
allowedTags.dispatchEvent(new Event('input'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
|
||||
// Immediately save the configuration. Intentionally do nothing that would
|
||||
// trigger an AJAX rebuild.
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextContains('Added text format');
|
||||
|
||||
// Verify that the configuration was saved.
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$page->clickLink('Style');
|
||||
$this->assertNotNull($styles_textarea = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]'));
|
||||
|
||||
$this->assertSame("p.foo.bar|Foobar paragraph\n", $styles_textarea->getValue());
|
||||
$assert_session->pageTextContains('One style configured');
|
||||
$allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$this->assertStringContainsString('<p class="foo bar">', $allowed_html_field->getValue());
|
||||
|
||||
// Attempt to use an unsupported HTML5 tag.
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
allowedTags.value = 's.redacted|Redacted';
|
||||
allowedTags.dispatchEvent(new Event('change'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
|
||||
// The CKEditor 5 module should refuse to specify styles on tags that cannot
|
||||
// (yet) be created.
|
||||
// @see \Drupal\ckeditor5\Plugin\Validation\Constraint\FundamentalCompatibilityConstraintValidator::checkAllHtmlTagsAreCreatable()
|
||||
$assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("The Style plugin needs another plugin to create <s>, for it to be able to create the following attributes: <s class="redacted">. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.")');
|
||||
// The entire vertical tab for "Style" settings should be marked up as the
|
||||
// cause of the error, which means the "Styles" text area in there is marked
|
||||
// too.
|
||||
$assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"][aria-invalid="true"]');
|
||||
$assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"] textarea[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"][aria-invalid="true"]');
|
||||
|
||||
// Attempt to save anyway: the warning should become an error.
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextNotContains('Added text format');
|
||||
$assert_session->elementExists('css', '[aria-label="Error message"]:contains("The Style plugin needs another plugin to create <s>, for it to be able to create the following attributes: <s class="redacted">. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.")');
|
||||
|
||||
// Now, attempt to use a supported non-HTML5 tag.
|
||||
// @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
allowedTags.value = 'drupal-media.sensational|Sensational media';
|
||||
allowedTags.dispatchEvent(new Event('change'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
|
||||
// The CKEditor 5 module should refuse to allow styles on non-HTML5 tags.
|
||||
$assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("A style can only be specified for an HTML 5 tag. <drupal-media> is not an HTML5 tag.")');
|
||||
// The vertical tab for "Style" settings should not be marked up as the
|
||||
// cause of the error, but only the "Styles" text area in the vertical tab.
|
||||
$assert_session->elementNotExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"][aria-invalid="true"]');
|
||||
$assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"] textarea[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"][aria-invalid="true"]');
|
||||
|
||||
// Test configuration overlaps across plugins.
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$this->assertNotEmpty($assert_session->elementExists('css', '.ckeditor5-toolbar-item-sourceEditing'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
// The Source Editing plugin settings form should now be present and should
|
||||
// have no allowed tags configured.
|
||||
$page->clickLink('Source editing');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]'));
|
||||
|
||||
// Make `<aside class>` creatable.
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]');
|
||||
allowedTags.value = '<aside class>';
|
||||
allowedTags.dispatchEvent(new Event('change'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Create a style with `aside` and a class name.
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
allowedTags.value = 'aside.error|Aside';
|
||||
allowedTags.dispatchEvent(new Event('change'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The CKEditor 5 module should refuse to create configuration overlaps
|
||||
// across plugins.
|
||||
// @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator::findStyleConflictingPluginLabel()
|
||||
$assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("A style must only specify classes not supported by other plugins.")');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests Style functionality: setting a class, expected style choices.
|
||||
*/
|
||||
public function testStyleFunctionality(): void {
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p class="highlighted interesting"> <br> <a href class="reliable"> <blockquote class="famous"> <h2 class="red-heading"> <ul class="items"> <ol class="steps"> <li> <table class="data-analysis"> <tr> <td rowspan colspan> <th rowspan colspan> <thead> <tbody> <tfoot> <caption class="caution"> <div class="deep-dive">',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'heading',
|
||||
'link',
|
||||
'blockQuote',
|
||||
'style',
|
||||
'bulletedList',
|
||||
'numberedList',
|
||||
'insertTable',
|
||||
'sourceEditing',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_heading' => [
|
||||
'enabled_headings' => [
|
||||
'heading2',
|
||||
],
|
||||
],
|
||||
'ckeditor5_list' => [
|
||||
'properties' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [
|
||||
'<div>',
|
||||
],
|
||||
],
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Highlighted & interesting',
|
||||
'element' => '<p class="highlighted interesting">',
|
||||
],
|
||||
[
|
||||
'label' => 'Red heading',
|
||||
'element' => '<h2 class="red-heading">',
|
||||
],
|
||||
[
|
||||
'label' => 'Reliable source',
|
||||
'element' => '<a class="reliable">',
|
||||
],
|
||||
[
|
||||
'label' => 'Famous',
|
||||
'element' => '<blockquote class="famous">',
|
||||
],
|
||||
[
|
||||
'label' => 'Items',
|
||||
'element' => '<ul class="items">',
|
||||
],
|
||||
[
|
||||
'label' => 'Steps',
|
||||
'element' => '<ol class="steps">',
|
||||
],
|
||||
[
|
||||
'label' => 'Data analysis',
|
||||
'element' => '<table class="data-analysis">',
|
||||
],
|
||||
[
|
||||
'label' => 'Truly deep dive',
|
||||
'element' => '<div class="deep-dive">',
|
||||
],
|
||||
[
|
||||
'label' => 'Caution caption',
|
||||
'element' => '<caption class="caution">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
|
||||
// Create a sample entity to test CKEditor 5.
|
||||
$node = $this->createNode([
|
||||
'type' => 'page',
|
||||
'title' => 'A selection of the history of Drupal',
|
||||
'body' => [
|
||||
'value' => '<h2>Upgrades</h2><p class="history">Drupal has historically been difficult to upgrade from one major version to the next.</p><p class="highlighted interesting">This changed with Drupal 8.</p><blockquote class="famous"><p>Updating from Drupal 8\'s latest version to Drupal 9.0.0 should be as easy as updating between minor versions of Drupal 8.</p></blockquote><p> — <a href="https://dri.es/making-drupal-upgrades-easy-forever">Dries</a></p><div><ul><li>Update Drupal core using Composer</li><li>Update Drupal core manually</li><li>Update Drupal core using Drush</li></ul><ol><li>Back up your files and database</li><li>Put your site into maintenance mode</li><li>Update the code and apply changes</li><li>Deactivate maintenance mode</li></ol><table><caption>Drupal upgrades are now easy, with a few caveats.</caption><tbody><tr><td>First</td><td>Second</td></tr><tr><td>Data value 1</td><td>Data value 2</td></tr></tbody></table></div>',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
// Observe.
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
]));
|
||||
|
||||
// Set a taller window size to ensure all possible style choices are in view
|
||||
// because otherwise Mink's getText() will return the empty string for those
|
||||
// out of view, despite the HTML showing that text.
|
||||
$this->getSession()->resizeWindow(1024, 1000);
|
||||
|
||||
$this->drupalGet($node->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
// Select the <h2>, assert that no style is active currently.
|
||||
$this->selectTextInsideElement('h2');
|
||||
$assert_session = $this->assertSession();
|
||||
$style_dropdown = $assert_session->elementExists('css', '.ck-style-dropdown');
|
||||
$this->assertSame('Styles', $style_dropdown->getText());
|
||||
|
||||
// Click the dropdown, check the available styles.
|
||||
$style_dropdown->click();
|
||||
$buttons = $style_dropdown->findAll('css', '.ck-dropdown__panel button');
|
||||
$this->assertCount(9, $buttons);
|
||||
|
||||
$this->assertSame('Highlighted & interesting', $buttons[0]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Red heading', $buttons[1]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Famous', $buttons[2]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Items', $buttons[3]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Steps', $buttons[4]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Data analysis', $buttons[5]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Truly deep dive', $buttons[6]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Caution caption', $buttons[7]->find('css', '.ck-button__label')->getText());
|
||||
// CKEditor's Style plugin first shows all block styles.
|
||||
for ($i = 0; $i <= 7; $i++) {
|
||||
$style_group = $buttons[$i]->getParent()->getParent();
|
||||
$this->assertTrue($style_group->hasClass('ck-style-panel__style-group'));
|
||||
$this->assertSame('Block styles', $style_group->find('css', 'label')->getText());
|
||||
}
|
||||
// And then all text styles.
|
||||
for ($i = 8; $i <= 8; $i++) {
|
||||
$style_group = $buttons[$i]->getParent()->getParent();
|
||||
$this->assertTrue($style_group->hasClass('ck-style-panel__style-group'));
|
||||
$this->assertSame('Text styles', $style_group->find('css', 'label')->getText());
|
||||
}
|
||||
$this->assertSame('Reliable source', $buttons[8]->find('css', '.ck-button__label')->getText());
|
||||
|
||||
$this->assertSame('true', $buttons[0]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[1]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[4]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[5]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[6]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[7]->getAttribute('aria-disabled'));
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
|
||||
// Apply the "Red heading" style and verify it has the expected effect.
|
||||
$assert_session->elementExists('css', '.ck-editor__main h2:not(.red-heading)');
|
||||
$buttons[1]->click();
|
||||
$assert_session->elementExists('css', '.ck-editor__main h2.red-heading');
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertSame('Red heading', $style_dropdown->getText());
|
||||
|
||||
// Select the first paragraph and observe changes in:
|
||||
// - styles dropdown label
|
||||
// - button states
|
||||
$this->selectTextInsideElement('p');
|
||||
$this->assertSame('Styles', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[4]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[5]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[6]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[7]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[8]->getAttribute('aria-disabled'));
|
||||
// Close the dropdown.
|
||||
$style_dropdown->click();
|
||||
|
||||
// Select the blockquote and observe changes in:
|
||||
// - styles dropdown label
|
||||
// - button states
|
||||
$this->selectTextInsideElement('blockquote');
|
||||
$this->assertSame('Famous', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
// TRICKY: the blockquote contains a paragraph.
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[2]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[4]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[5]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[6]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[7]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[8]->getAttribute('aria-disabled'));
|
||||
// Close the dropdown.
|
||||
$style_dropdown->click();
|
||||
|
||||
// Select the <ul> and check the available styles
|
||||
$this->selectTextInsideElement('ul');
|
||||
$this->assertSame('Styles', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
// TRICKY: the contents of the list item can be converted to a paragraph.
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[3]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[4]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[5]->getAttribute('aria-disabled'));
|
||||
// TRICKY: the <ul> is wrapped in a <div>, so the "Truly deep dive" <div>
|
||||
// style is available!
|
||||
$this->assertFalse($buttons[6]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[7]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[8]->getAttribute('aria-disabled'));
|
||||
|
||||
// Apply the "Items" style and verify it has the expected effect.
|
||||
$assert_session->elementExists('css', '.ck-editor__main ul:not(.items)');
|
||||
$buttons[3]->click();
|
||||
$assert_session->elementExists('css', '.ck-editor__main ul.items');
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertSame('Items', $style_dropdown->getText());
|
||||
|
||||
// Select the <ol> and check the available styles
|
||||
$this->selectTextInsideElement('ol');
|
||||
$this->assertSame('Styles', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
// TRICKY: the contents of the list item can be converted to a paragraph.
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[4]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[5]->getAttribute('aria-disabled'));
|
||||
// TRICKY: the <ol> is wrapped in a <div>, so the "Truly deep dive" <div>
|
||||
// style is available!
|
||||
$this->assertFalse($buttons[6]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[7]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[8]->getAttribute('aria-disabled'));
|
||||
|
||||
// Apply the "Steps" style and verify it has the expected effect.
|
||||
$assert_session->elementExists('css', '.ck-editor__main ol:not(.steps)');
|
||||
$buttons[4]->click();
|
||||
$assert_session->elementExists('css', '.ck-editor__main ol.steps');
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertSame('Steps', $style_dropdown->getText());
|
||||
|
||||
// Select the table and check the available styles
|
||||
$this->selectTextInsideElement('table td');
|
||||
$this->assertSame('Styles', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
// TRICKY: the contents of the table cell can be converted to a paragraph.
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[4]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[5]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[6]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[7]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[8]->getAttribute('aria-disabled'));
|
||||
|
||||
// Apply the "Data analysis" style and verify it has the expected effect.
|
||||
$assert_session->elementExists('css', '.ck-editor__main table:not(.data-analysis)');
|
||||
$buttons[5]->click();
|
||||
$assert_session->elementExists('css', '.ck-editor__main table.data-analysis');
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertSame('Data analysis', $style_dropdown->getText());
|
||||
|
||||
// Select the link, assert that no style is active currently.
|
||||
$this->selectTextInsideElement('a');
|
||||
$this->assertSame('Styles', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
// TRICKY: the link is inside a paragraph.
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[4]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[5]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[6]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[7]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[8]->hasAttribute('aria-disabled'));
|
||||
|
||||
// Apply the "Reliable source" style and verify it has the expected effect.
|
||||
$assert_session->elementExists('css', '.ck-editor__main a:not(.reliable)');
|
||||
$buttons[8]->click();
|
||||
$assert_session->elementExists('css', '.ck-editor__main a.reliable');
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-on'));
|
||||
$this->assertSame('Reliable source', $style_dropdown->getText());
|
||||
|
||||
// Because we cannot select the <div> directly (it's not a visible element),
|
||||
// select the <ol> AGAIN and check the available styles — because we should
|
||||
// be able to change the containing <div>'s style through here. Note that we
|
||||
// already activated the "Steps" style previously.
|
||||
$this->selectTextInsideElement('ol');
|
||||
$this->assertSame('Steps', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
// TRICKY: the contents of the list item can be converted to a paragraph.
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[4]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[5]->getAttribute('aria-disabled'));
|
||||
// TRICKY: the <ol> is wrapped in a <div>, so the "Truly deep dive" <div>
|
||||
// style is available!
|
||||
$this->assertFalse($buttons[6]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[7]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[8]->getAttribute('aria-disabled'));
|
||||
|
||||
// Apply the "Truly deep dive" style and verify it has the expected effect.
|
||||
$assert_session->elementExists('css', '.ck-editor__main div:not(.deep-dive)');
|
||||
$buttons[6]->click();
|
||||
$assert_session->elementExists('css', '.ck-editor__main div.deep-dive');
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertSame('Multiple styles', $style_dropdown->getText());
|
||||
|
||||
// Select the table caption, assert that no style is active currently.
|
||||
$this->selectTextInsideElement('figure.table > figcaption');
|
||||
$this->assertSame('Data analysis', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
// TRICKY: the caption is inside the table, so the "Data analysis" style is
|
||||
// also active.
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertSame('true', $buttons[0]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[4]->getAttribute('aria-disabled'));
|
||||
// TRICKY: the caption is inside the table, so the "Data analysis" style is
|
||||
// also active.
|
||||
$this->assertFalse($buttons[5]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[6]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[7]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[8]->getAttribute('aria-disabled'));
|
||||
|
||||
// Apply the "Caution caption" style and verify it has the expected effect.
|
||||
$assert_session->elementExists('css', '.ck-editor__main figure.table > figcaption:not(.caution)');
|
||||
$buttons[7]->click();
|
||||
$assert_session->elementExists('css', '.ck-editor__main figure.table > figcaption.caution');
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[4]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[5]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[6]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[7]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[8]->hasClass('ck-off'));
|
||||
$this->assertSame('Multiple styles', $style_dropdown->getText());
|
||||
|
||||
// The resulting markup should be identical to the starting markup, with
|
||||
// seven changes:
|
||||
// 1. the `red-heading` class has been added to the `<h2>`
|
||||
// 2. the `history` class has been removed from the `<p>`, because CKEditor
|
||||
// 5 has not been configured for this: if a Style had configured for it,
|
||||
// it would have been retained.
|
||||
// 3. the `items` class has been added to the `<ul>`
|
||||
// 4. the `steps` class has been added to the `<ol>`
|
||||
// 5. the `data-analysis` class has been added to the `<table>`
|
||||
// 6. the `reliable` class has been added to the `<a>`
|
||||
// 7. The `deep-dive` class has been added to the `<div>`
|
||||
// 8. The `caution` class has been added to the `<caption>`
|
||||
$this->assertSame('<h2 class="red-heading">Upgrades</h2><p>Drupal has historically been difficult to upgrade from one major version to the next.</p><p class="highlighted interesting">This changed with Drupal 8.</p><blockquote class="famous"><p>Updating from Drupal 8\'s latest version to Drupal 9.0.0 should be as easy as updating between minor versions of Drupal 8.</p></blockquote><p>— <a class="reliable" href="https://dri.es/making-drupal-upgrades-easy-forever">Dries</a></p><div class="deep-dive"><ul class="items"><li>Update Drupal core using Composer</li><li>Update Drupal core manually</li><li>Update Drupal core using Drush</li></ul><ol class="steps"><li>Back up your files and database</li><li>Put your site into maintenance mode</li><li>Update the code and apply changes</li><li>Deactivate maintenance mode</li></ol><table class="table data-analysis"><caption class="caution">Drupal upgrades are now easy, with a few caveats.</caption><tbody><tr><td>First</td><td>Second</td></tr><tr><td>Data value 1</td><td>Data value 2</td></tr></tbody></table></div>', $this->getEditorDataAsHtmlString());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* For testing the table plugin.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class TableTest extends WebDriverTestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* A host entity with a body field to embed images in.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* Text added to captions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $captionText = 'some caption';
|
||||
|
||||
/**
|
||||
* Text added to table cells.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableCellText = 'table cell';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
'node',
|
||||
'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<br> <p> <table> <tr> <td rowspan colspan> <th rowspan colspan> <thead> <tbody> <tfoot> <caption>',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'insertTable',
|
||||
'sourceEditing',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
|
||||
// Create a sample host entity.
|
||||
$this->host = $this->createNode([
|
||||
'type' => 'page',
|
||||
'title' => 'Animals with strange names',
|
||||
'body' => [
|
||||
'value' => '<p>some content that will likely change</p>',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms tables convert to the expected markup.
|
||||
*/
|
||||
public function testTableConversion(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
// This is CKEditor 5's default table markup, but uses elements that are
|
||||
// not allowed by the text format.
|
||||
$this->host->body->value = '<figure class="table"><table><tbody><tr><td>table cell</td></tr></tbody></table> <figcaption>some caption</figcaption></figure>';
|
||||
$this->host->save();
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
|
||||
$this->captionText = 'some caption';
|
||||
$this->tableCellText = 'table cell';
|
||||
|
||||
$table_container = $assert_session->waitForElementVisible('css', 'figure.table');
|
||||
$this->assertNotNull($table_container);
|
||||
$caption = $page->find('css', 'figure.table > figcaption');
|
||||
$this->assertEquals($this->captionText, $caption->getText());
|
||||
$table = $page->find('css', 'figure.table > table');
|
||||
$this->assertEquals($this->tableCellText, $table->getText());
|
||||
|
||||
$this->assertTableStructureInEditorData();
|
||||
$this->assertTableStructureInRenderedPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests creating a table with caption in the UI.
|
||||
*/
|
||||
public function testTableCaptionUi(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
// Add a table via the editor buttons.
|
||||
$table_button = $page->find('css', '.ck-dropdown button');
|
||||
$table_button->click();
|
||||
|
||||
// Add a single table cell.
|
||||
$grid_button = $assert_session->waitForElementVisible('css', '.ck-insert-table-dropdown-grid-box[data-row="1"][data-column="1"]');
|
||||
$grid_button->click();
|
||||
|
||||
// Confirm the table has been added and no caption is present.
|
||||
$this->assertNotNull($table_figure = $assert_session->waitForElementVisible('css', 'figure.table'));
|
||||
$assert_session->elementNotExists('css', 'figure.table > figcaption');
|
||||
|
||||
// Enable captions and update caption content.
|
||||
$caption_button = $this->getBalloonButton('Toggle caption on');
|
||||
$caption_button->click();
|
||||
$caption = $assert_session->waitForElementVisible('css', 'figure.table > figcaption');
|
||||
$this->assertEmpty($caption->getText());
|
||||
$caption->setValue($this->captionText);
|
||||
$this->assertEquals($this->captionText, $caption->getText());
|
||||
|
||||
// Update table cell content.
|
||||
$table_cell = $assert_session->waitForElement('css', '.ck-editor__nested-editable .ck-table-bogus-paragraph');
|
||||
$this->assertNotEmpty($table_cell);
|
||||
$table_cell->click();
|
||||
$table_cell->setValue($this->tableCellText);
|
||||
$table_cell = $page->find('css', 'figure.table > table > tbody > tr > td');
|
||||
$this->assertEquals($this->tableCellText, $table_cell->getText());
|
||||
|
||||
$this->assertTableStructureInEditorData();
|
||||
|
||||
// Disable caption, confirm the caption is no longer present.
|
||||
$table_figure->click();
|
||||
$caption_off_button = $this->getBalloonButton('Toggle caption off');
|
||||
$caption_off_button->click();
|
||||
$assert_session->assertNoElementAfterWait('css', 'figure.table > figcaption');
|
||||
|
||||
// Re-enable caption and confirm the value was retained.
|
||||
$table_figure->click();
|
||||
$caption_on_button = $this->getBalloonButton('Toggle caption on');
|
||||
$caption_on_button->click();
|
||||
$caption = $assert_session->waitForElementVisible('css', 'figure.table > figcaption');
|
||||
$this->assertEquals($this->captionText, $caption->getText());
|
||||
|
||||
$this->assertTableStructureInRenderedPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the structure of the table within the editor data.
|
||||
*/
|
||||
public function assertTableStructureInEditorData(): void {
|
||||
$xpath = new \DOMXPath($this->getEditorDataAsDom());
|
||||
$this->assertEmpty($xpath->query('//figure'), 'There should be no figure tag in editor data');
|
||||
$this->assertNotEmpty($xpath->query('//table/caption'), 'A caption should be the immediate child of <table>');
|
||||
$this->assertEquals($this->captionText, (string) $xpath->query('//table/caption')[0]->nodeValue, "The caption should say {$this->captionText}");
|
||||
$this->assertNotEmpty($xpath->query('//table/tbody/tr/td'), 'There is an expected table structure.');
|
||||
$this->assertEquals($this->tableCellText, (string) $xpath->query('//table/tbody/tr/td')[0]->nodeValue, "The table cell should say {$this->tableCellText}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the saved page has the expected table structure.
|
||||
*/
|
||||
public function assertTableStructureInRenderedPage(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$page->pressButton('Save');
|
||||
$assert_session->waitForText('has been updated');
|
||||
|
||||
$assert_session->pageTextContains($this->tableCellText);
|
||||
$assert_session->pageTextContains($this->captionText);
|
||||
|
||||
$assert_session->elementNotExists('css', 'figure');
|
||||
$this->assertNotNull($table_cell = $page->find('css', 'table > tbody > tr > td'), 'Table on rendered page has expected structure');
|
||||
$this->assertEquals($this->tableCellText, $table_cell->getText(), 'Table on rendered page has expected content');
|
||||
$this->assertNotNull($table_caption = $page->find('css', 'table > caption '), 'Table caption is in expected structure.');
|
||||
$this->assertEquals($this->captionText, $table_caption->getText(), 'Table caption has expected text');
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Test the ckeditor5-stylesheets theme config property.
|
||||
*
|
||||
* @group ckeditor5
|
||||
*/
|
||||
class CKEditor5StylesheetsTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'system',
|
||||
'user',
|
||||
'ckeditor5',
|
||||
'editor',
|
||||
'filter',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests loading of theme's CKEditor 5 stylesheets defined in the .info file.
|
||||
*
|
||||
* @param string $theme
|
||||
* The machine name of the theme.
|
||||
* @param array $expected
|
||||
* The expected CKEditor 5 CSS paths from the theme.
|
||||
*
|
||||
* @dataProvider externalStylesheetsProvider
|
||||
*/
|
||||
public function testExternalStylesheets($theme, $expected): void {
|
||||
\Drupal::service('theme_installer')->install([$theme]);
|
||||
$this->config('system.theme')->set('default', $theme)->save();
|
||||
$this->assertSame($expected, _ckeditor5_theme_css($theme));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for external stylesheets.
|
||||
*
|
||||
* @return array
|
||||
* An array of test cases.
|
||||
*/
|
||||
public static function externalStylesheetsProvider() {
|
||||
return [
|
||||
'Install theme which has an absolute external CSS URL' => [
|
||||
'test_ckeditor_stylesheets_external',
|
||||
['https://fonts.googleapis.com/css?family=Open+Sans'],
|
||||
],
|
||||
'Install theme which has an external protocol-relative CSS URL' => [
|
||||
'test_ckeditor_stylesheets_protocol_relative',
|
||||
['//fonts.googleapis.com/css?family=Open+Sans'],
|
||||
],
|
||||
'Install theme which has a relative CSS URL' => [
|
||||
'test_ckeditor_stylesheets_relative',
|
||||
['/core/modules/system/tests/themes/test_ckeditor_stylesheets_relative/css/yokotsoko.css'],
|
||||
],
|
||||
'Install theme which has a Drupal root CSS URL' => [
|
||||
'test_ckeditor_stylesheets_drupal_root',
|
||||
['/core/modules/system/tests/themes/test_ckeditor_stylesheets_drupal_root/css/yokotsoko.css'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Kernel;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
|
||||
/**
|
||||
* Defines a trait for testing CKEditor 5 validity.
|
||||
*/
|
||||
trait CKEditor5ValidationTestTrait {
|
||||
|
||||
/**
|
||||
* Decorator for CKEditor5::validatePair() that returns an assertable array.
|
||||
*
|
||||
* @param \Drupal\editor\EditorInterface $text_editor
|
||||
* The paired text editor to validate.
|
||||
* @param \Drupal\filter\FilterFormatInterface $text_format
|
||||
* The paired text format to validate.
|
||||
* @param bool $all_compatibility_problems
|
||||
* Only fundamental compatibility violations are returned unless TRUE.
|
||||
*
|
||||
* @return array
|
||||
* An array with property paths as keys and violation messages as values.
|
||||
*
|
||||
* @see \Drupal\ckeditor5\Plugin\Editor\CKEditor5::validatePair
|
||||
*/
|
||||
private function validatePairToViolationsArray(EditorInterface $text_editor, FilterFormatInterface $text_format, bool $all_compatibility_problems): array {
|
||||
$violations = CKEditor5::validatePair($text_editor, $text_format, $all_compatibility_problems);
|
||||
return self::violationsToArray($violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a constraint violation list object to an assertable array.
|
||||
*
|
||||
* @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
|
||||
* Validation constraint violations.
|
||||
*
|
||||
* @return array
|
||||
* An array with property paths as keys and violation messages as values.
|
||||
*/
|
||||
private static function violationsToArray(ConstraintViolationListInterface $violations): array {
|
||||
$actual_violations = [];
|
||||
foreach ($violations as $violation) {
|
||||
if (!isset($actual_violations[$violation->getPropertyPath()])) {
|
||||
$actual_violations[$violation->getPropertyPath()] = (string) $violation->getMessage();
|
||||
}
|
||||
else {
|
||||
// Transform value from string to array.
|
||||
if (is_string($actual_violations[$violation->getPropertyPath()])) {
|
||||
$actual_violations[$violation->getPropertyPath()] = (array) $actual_violations[$violation->getPropertyPath()];
|
||||
}
|
||||
// And append.
|
||||
$actual_violations[$violation->getPropertyPath()][] = (string) $violation->getMessage();
|
||||
}
|
||||
}
|
||||
return $actual_violations;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Kernel\ConfigAction;
|
||||
|
||||
use Drupal\Core\Config\Action\ConfigActionException;
|
||||
use Drupal\Core\Recipe\InvalidConfigException;
|
||||
use Drupal\Core\Recipe\RecipeRunner;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* @covers \Drupal\ckeditor5\Plugin\ConfigAction\AddItemToToolbar
|
||||
* @group ckeditor5
|
||||
* @group Recipe
|
||||
*/
|
||||
class AddItemToToolbarConfigActionTest extends KernelTestBase {
|
||||
|
||||
use RecipeTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
'editor',
|
||||
'filter',
|
||||
'filter_test',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $configSchemaCheckerExclusions = [
|
||||
// This test must be allowed to save invalid config, we can confirm that
|
||||
// any invalid stuff is validated by the config actions system.
|
||||
'editor.editor.filter_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->installConfig('filter_test');
|
||||
|
||||
$editor = Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'filter_test',
|
||||
'image_upload' => ['status' => FALSE],
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
/** @var array{toolbar: array{items: array<int, string>}} $settings */
|
||||
$settings = Editor::load('filter_test')?->getSettings();
|
||||
$this->assertSame(['heading', 'bold', 'italic'], $settings['toolbar']['items']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array<string, mixed> $action
|
||||
* The value to pass to the config action.
|
||||
* @param string[] $expected_toolbar_items
|
||||
* The items which should be in the editor toolbar, in the expected order.
|
||||
*
|
||||
* @testWith ["sourceEditing", ["heading", "bold", "italic", "sourceEditing"]]
|
||||
* [{"item_name": "sourceEditing"}, ["heading", "bold", "italic", "sourceEditing"]]
|
||||
* [{"item_name": "sourceEditing", "position": 1}, ["heading", "sourceEditing", "bold", "italic"]]
|
||||
* [{"item_name": "sourceEditing", "position": 1, "replace": true}, ["heading", "sourceEditing", "italic"]]
|
||||
* [{"item_name": "bold"}, ["heading", "bold", "italic"]]
|
||||
* [{"item_name": "bold", "allow_duplicate": true}, ["heading", "bold", "italic", "bold"]]
|
||||
*/
|
||||
public function testAddItemToToolbar(string|array $action, array $expected_toolbar_items): void {
|
||||
$recipe = $this->createRecipe([
|
||||
'name' => 'CKEditor 5 toolbar item test',
|
||||
'config' => [
|
||||
'actions' => [
|
||||
'editor.editor.filter_test' => [
|
||||
'addItemToToolbar' => $action,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
RecipeRunner::processRecipe($recipe);
|
||||
|
||||
/** @var array{toolbar: array{items: string[]}, plugins: array<string, array<mixed>>} $settings */
|
||||
$settings = Editor::load('filter_test')?->getSettings();
|
||||
$this->assertSame($expected_toolbar_items, $settings['toolbar']['items']);
|
||||
|
||||
// The plugin's default settings should have been added.
|
||||
if (in_array('sourceEditing', $expected_toolbar_items, TRUE)) {
|
||||
$this->assertSame([], $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that adding non-existent toolbar item to CKEditor triggers an error.
|
||||
*/
|
||||
public function testAddNonExistentItem(): void {
|
||||
$recipe = $this->createRecipe([
|
||||
'name' => 'Add an invalid toolbar item',
|
||||
'config' => [
|
||||
'actions' => [
|
||||
'editor.editor.filter_test' => [
|
||||
'addItemToToolbar' => 'bogus_item',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->expectException(InvalidConfigException::class);
|
||||
$this->expectExceptionMessage("There were validation errors in editor.editor.filter_test:\n- settings.toolbar.items.3: The provided toolbar item <em class=\"placeholder\">bogus_item</em> is not valid.");
|
||||
RecipeRunner::processRecipe($recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the `addItemToToolbar` config action requires CKEditor 5.
|
||||
*/
|
||||
public function testActionRequiresCKEditor5(): void {
|
||||
$this->enableModules(['editor_test']);
|
||||
Editor::load('filter_test')?->setEditor('unicorn')->setSettings([])->save();
|
||||
|
||||
$recipe = <<<YAML
|
||||
name: Not a CKEditor
|
||||
config:
|
||||
actions:
|
||||
editor.editor.filter_test:
|
||||
addItemToToolbar: strikethrough
|
||||
YAML;
|
||||
|
||||
$this->expectException(ConfigActionException::class);
|
||||
$this->expectExceptionMessage('The editor:addItemToToolbar config action only works with editors that use CKEditor 5.');
|
||||
RecipeRunner::processRecipe($this->createRecipe($recipe));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Kernel;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests configurable plugins.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class ConfigurablePluginTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
// These modules must be installed for ckeditor5_config_schema_info_alter()
|
||||
// to work, which in turn is necessary for the plugin definition validation
|
||||
// logic.
|
||||
// @see \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition::validateDrupalAspects()
|
||||
'filter',
|
||||
'editor',
|
||||
];
|
||||
|
||||
/**
|
||||
* The manager for "CKEditor 5 plugin" plugins.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->manager = $this->container->get('plugin.manager.ckeditor5.plugin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests default settings for configurable CKEditor 5 plugins.
|
||||
*/
|
||||
public function testDefaults(): void {
|
||||
$all_definitions = $this->manager->getDefinitions();
|
||||
$configurable_definitions = array_filter($all_definitions, function (CKEditor5PluginDefinition $definition): bool {
|
||||
return $definition->isConfigurable();
|
||||
});
|
||||
$default_plugin_settings = [];
|
||||
foreach (array_keys($configurable_definitions) as $plugin_name) {
|
||||
$default_plugin_settings[$plugin_name] = $this->manager->getPlugin($plugin_name, NULL)->defaultConfiguration();
|
||||
}
|
||||
|
||||
$expected_default_plugin_settings = [
|
||||
'ckeditor5_heading' => [
|
||||
'enabled_headings' => [
|
||||
'heading2',
|
||||
'heading3',
|
||||
'heading4',
|
||||
'heading5',
|
||||
'heading6',
|
||||
],
|
||||
],
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [],
|
||||
],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
'ckeditor5_codeBlock' => [
|
||||
'languages' => [
|
||||
['language' => 'plaintext', 'label' => 'Plain text'],
|
||||
['language' => 'c', 'label' => 'C'],
|
||||
['language' => 'cs', 'label' => 'C#'],
|
||||
['language' => 'cpp', 'label' => 'C++'],
|
||||
['language' => 'css', 'label' => 'CSS'],
|
||||
['language' => 'diff', 'label' => 'Diff'],
|
||||
['language' => 'html', 'label' => 'HTML'],
|
||||
['language' => 'java', 'label' => 'Java'],
|
||||
['language' => 'javascript', 'label' => 'JavaScript'],
|
||||
['language' => 'php', 'label' => 'PHP'],
|
||||
['language' => 'python', 'label' => 'Python'],
|
||||
['language' => 'ruby', 'label' => 'Ruby'],
|
||||
['language' => 'typescript', 'label' => 'TypeScript'],
|
||||
['language' => 'xml', 'label' => 'XML'],
|
||||
],
|
||||
],
|
||||
'ckeditor5_list' => [
|
||||
'properties' => [
|
||||
'reversed' => TRUE,
|
||||
'startIndex' => TRUE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
'ckeditor5_alignment' => [
|
||||
'enabled_alignments' => [
|
||||
0 => 'left',
|
||||
1 => 'center',
|
||||
2 => 'right',
|
||||
3 => 'justify',
|
||||
],
|
||||
],
|
||||
'ckeditor5_image' => [],
|
||||
'ckeditor5_imageResize' => [
|
||||
'allow_resize' => TRUE,
|
||||
],
|
||||
'ckeditor5_language' => [
|
||||
'language_list' => 'un',
|
||||
],
|
||||
];
|
||||
$this->assertSame($expected_default_plugin_settings, $default_plugin_settings);
|
||||
}
|
||||
|
||||
}
|
||||
127
web/core/modules/ckeditor5/tests/src/Kernel/LanguageTest.php
Normal file
127
web/core/modules/ckeditor5/tests/src/Kernel/LanguageTest.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Kernel;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\TestTools\Random;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* Tests language resolving for CKEditor 5.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class LanguageTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* The CKEditor 5 plugin.
|
||||
*
|
||||
* @var \Drupal\ckeditor5\Plugin\Editor\CKEditor5
|
||||
*/
|
||||
protected $ckeditor5;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'system',
|
||||
'user',
|
||||
'ckeditor5',
|
||||
'editor',
|
||||
'filter',
|
||||
'language',
|
||||
'locale',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->ckeditor5 = $this->container->get('plugin.manager.editor')->createInstance('ckeditor5');
|
||||
|
||||
FilterFormat::create(
|
||||
Yaml::parseFile('core/profiles/standard/config/install/filter.format.basic_html.yml')
|
||||
)->save();
|
||||
Editor::create([
|
||||
'format' => 'basic_html',
|
||||
'editor' => 'ckeditor5',
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
|
||||
$this->installConfig(['language']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that languages are resolved correctly.
|
||||
*
|
||||
* @param string $drupal_langcode
|
||||
* The language code in Drupal.
|
||||
* @param string $cke5_langcode
|
||||
* The language code in CKEditor 5.
|
||||
* @param bool $is_missing_mapping
|
||||
* Whether this mapping is expected to be missing from language.mappings.
|
||||
*
|
||||
* @dataProvider provider
|
||||
*/
|
||||
public function test(string $drupal_langcode, string $cke5_langcode, bool $is_missing_mapping = FALSE): void {
|
||||
$editor = Editor::load('basic_html');
|
||||
|
||||
ConfigurableLanguage::createFromLangcode($drupal_langcode)->save();
|
||||
$this->config('system.site')->set('default_langcode', $drupal_langcode)->save();
|
||||
|
||||
if ($is_missing_mapping) {
|
||||
// CKEditor 5's UI language falls back to English, until the language
|
||||
// mapping is expanded.
|
||||
$settings = $this->ckeditor5->getJSSettings($editor);
|
||||
$this->assertSame('en', $settings['language']['ui']);
|
||||
|
||||
// Expand the language mapping.
|
||||
$config = $this->config('language.mappings');
|
||||
$mapping = $config->get('map');
|
||||
$mapping += [$cke5_langcode => $drupal_langcode];
|
||||
$config->set('map', $mapping)->save();
|
||||
}
|
||||
|
||||
$settings = $this->ckeditor5->getJSSettings($editor);
|
||||
$this->assertSame($cke5_langcode, $settings['language']['ui']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of language code pairs.
|
||||
*
|
||||
* @return string[][]
|
||||
* An array of language code pairs.
|
||||
*/
|
||||
public static function provider(): array {
|
||||
$random_langcode = Random::machineName();
|
||||
return [
|
||||
'Language code transformed from browser mappings' => [
|
||||
'drupal_langcode' => 'pt-pt',
|
||||
'cke5_langcode' => 'pt',
|
||||
],
|
||||
'Language code transformed from browser mappings 2' => [
|
||||
'drupal_langcode' => 'zh-hans',
|
||||
'cke5_langcode' => 'zh-cn',
|
||||
],
|
||||
'Language code both in Drupal and CKEditor' => [
|
||||
'drupal_langcode' => 'fi',
|
||||
'cke5_langcode' => 'fi',
|
||||
],
|
||||
'Language code not in Drupal but in CKEditor 5 requires new language.mappings entry' => [
|
||||
'drupal_langcode' => $random_langcode,
|
||||
'cke5_langcode' => 'de-ch',
|
||||
'is_missing_mapping' => TRUE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1643
web/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
Normal file
1643
web/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Kernel;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class WildcardHtmlSupportTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor5',
|
||||
'filter',
|
||||
'editor',
|
||||
];
|
||||
|
||||
/**
|
||||
* The manager for "CKEditor 5 plugin" plugins.
|
||||
*
|
||||
* @var \Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->manager = $this->container->get('plugin.manager.ckeditor5.plugin');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing::getDynamicPluginConfig
|
||||
* @dataProvider providerGhsConfiguration
|
||||
*/
|
||||
public function testGhsConfiguration(string $filter_html_allowed, array $source_editing_tags, array $expected_ghs_configuration, ?array $additional_toolbar_items = []): void {
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => $filter_html_allowed,
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
$editor_config = [
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => array_merge(['sourceEditing'], $additional_toolbar_items),
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => $source_editing_tags,
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
];
|
||||
if (in_array('alignment', $additional_toolbar_items, TRUE)) {
|
||||
$editor_config['settings']['plugins']['ckeditor5_alignment'] = [
|
||||
'enabled_alignments' => ['left', 'center', 'right', 'justify'],
|
||||
];
|
||||
}
|
||||
|
||||
$editor = Editor::create($editor_config);
|
||||
$editor->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolationInterface $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
$config = $this->manager->getCKEditor5PluginConfig($editor);
|
||||
$ghs_configuration = $config['config']['htmlSupport']['allow'];
|
||||
// The first two entries in the GHS configuration are from the
|
||||
// `ckeditor5_globalAttributeDir` and `ckeditor5_globalAttributeLang`
|
||||
// plugins. They are out of scope for this test, so omit them.
|
||||
$ghs_configuration = array_slice($ghs_configuration, 2);
|
||||
$this->assertEquals($expected_ghs_configuration, $ghs_configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test cases for CKEditor 5 General HTML Support (GHS) configuration.
|
||||
*/
|
||||
public static function providerGhsConfiguration(): array {
|
||||
return [
|
||||
'empty source editing' => [
|
||||
'<p> <br>',
|
||||
[],
|
||||
[],
|
||||
],
|
||||
'without wildcard' => [
|
||||
'<p> <br> <a href> <blockquote> <div data-llama>',
|
||||
['<div data-llama>'],
|
||||
[
|
||||
[
|
||||
'name' => 'div',
|
||||
'attributes' => [
|
||||
[
|
||||
'key' => 'data-llama',
|
||||
'value' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
['link', 'blockQuote'],
|
||||
],
|
||||
'<$text-container> minimal configuration' => [
|
||||
'<p data-llama> <br>',
|
||||
['<$text-container data-llama>'],
|
||||
[
|
||||
[
|
||||
'name' => 'p',
|
||||
'attributes' => [
|
||||
[
|
||||
'key' => 'data-llama',
|
||||
'value' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'<$text-container> from multiple plugins' => [
|
||||
'<p data-llama class="text-align-left text-align-center text-align-right text-align-justify"> <br>',
|
||||
['<$text-container data-llama>'],
|
||||
[
|
||||
[
|
||||
'name' => 'p',
|
||||
'attributes' => [
|
||||
[
|
||||
'key' => 'data-llama',
|
||||
'value' => TRUE,
|
||||
],
|
||||
],
|
||||
'classes' => [
|
||||
'regexp' => [
|
||||
'pattern' => '/^(text-align-left|text-align-center|text-align-right|text-align-justify)$/',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
['alignment'],
|
||||
],
|
||||
'<$text-container> with attribute from multiple plugins' => [
|
||||
'<p data-llama class> <br>',
|
||||
['<$text-container data-llama>', '<p class>'],
|
||||
[
|
||||
[
|
||||
'name' => 'p',
|
||||
'classes' => TRUE,
|
||||
],
|
||||
[
|
||||
'name' => 'p',
|
||||
'attributes' => [
|
||||
[
|
||||
'key' => 'data-llama',
|
||||
'value' => TRUE,
|
||||
],
|
||||
],
|
||||
'classes' => [
|
||||
'regexp' => [
|
||||
'pattern' => '/^(text-align-left|text-align-center|text-align-right|text-align-justify)$/',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
['alignment'],
|
||||
],
|
||||
'<$text-container> realistic configuration' => [
|
||||
'<p data-llama> <br> <a href> <blockquote> <div data-llama> <mark> <abbr title>',
|
||||
['<$text-container data-llama>', '<div>', '<mark>', '<abbr title>'],
|
||||
[
|
||||
[
|
||||
'name' => 'div',
|
||||
],
|
||||
[
|
||||
'name' => 'mark',
|
||||
],
|
||||
[
|
||||
'name' => 'abbr',
|
||||
'attributes' => [
|
||||
[
|
||||
'key' => 'title',
|
||||
'value' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'p',
|
||||
'attributes' => [
|
||||
[
|
||||
'key' => 'data-llama',
|
||||
'value' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'div',
|
||||
'attributes' => [
|
||||
[
|
||||
'key' => 'data-llama',
|
||||
'value' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
['link', 'blockQuote'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,163 @@
|
||||
// cspell:ignore drupalhtmlbuilder
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
// Nightwatch doesn't support ES modules. This workaround loads the class
|
||||
// directly here.
|
||||
// @todo remove this after https://www.drupal.org/project/drupal/issues/3247647
|
||||
// has been resolved.
|
||||
// eslint-disable-next-line no-eval
|
||||
const DrupalHtmlBuilder = eval(
|
||||
`(${fs
|
||||
.readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../../../js/ckeditor5_plugins/drupalHtmlEngine/src/drupalhtmlbuilder.js',
|
||||
),
|
||||
)
|
||||
.toString()})`.replace('export default', ''),
|
||||
);
|
||||
const { document, Node } = new JSDOM(`<!DOCTYPE html>`).window;
|
||||
|
||||
module.exports = {
|
||||
'@tags': ['ckeditor5'],
|
||||
'@unitTest': true,
|
||||
'should return empty string when empty DocumentFragment is passed':
|
||||
function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
drupalHtmlBuilder.appendNode(document.createDocumentFragment());
|
||||
assert.equal(drupalHtmlBuilder.build(), '');
|
||||
},
|
||||
'should create text from single text node': function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
const text = 'foo bar';
|
||||
const fragment = document.createDocumentFragment();
|
||||
const textNode = document.createTextNode(text);
|
||||
fragment.appendChild(textNode);
|
||||
|
||||
drupalHtmlBuilder.appendNode(fragment);
|
||||
assert.equal(drupalHtmlBuilder.build(), text);
|
||||
},
|
||||
'should return correct HTML from fragment with paragraph': function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
const fragment = document.createDocumentFragment();
|
||||
const paragraph = document.createElement('p');
|
||||
paragraph.textContent = 'foo bar';
|
||||
fragment.appendChild(paragraph);
|
||||
|
||||
drupalHtmlBuilder.appendNode(fragment);
|
||||
assert.equal(drupalHtmlBuilder.build(), '<p>foo bar</p>');
|
||||
},
|
||||
'should return correct HTML from fragment with multiple child nodes':
|
||||
function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
const fragment = document.createDocumentFragment();
|
||||
const text = document.createTextNode('foo bar');
|
||||
const paragraph = document.createElement('p');
|
||||
const div = document.createElement('div');
|
||||
|
||||
paragraph.textContent = 'foo';
|
||||
div.textContent = 'bar';
|
||||
|
||||
fragment.appendChild(text);
|
||||
fragment.appendChild(paragraph);
|
||||
fragment.appendChild(div);
|
||||
|
||||
drupalHtmlBuilder.appendNode(fragment);
|
||||
|
||||
assert.equal(
|
||||
drupalHtmlBuilder.build(),
|
||||
'foo bar<p>foo</p><div>bar</div>',
|
||||
);
|
||||
},
|
||||
'should return correct HTML scripts and styles': function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
const fragment = document.createDocumentFragment();
|
||||
const script = document.createElement('script');
|
||||
script.textContent = `let x = 10;
|
||||
let y = 5;
|
||||
if (y < x) {
|
||||
console.log('is smaller')
|
||||
}`;
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('type', 'text/css');
|
||||
style.appendChild(
|
||||
document.createTextNode(':root .sections > h2 { background: red}'),
|
||||
);
|
||||
|
||||
fragment.appendChild(style);
|
||||
fragment.appendChild(document.createTextNode('\n'));
|
||||
fragment.appendChild(script);
|
||||
|
||||
drupalHtmlBuilder.appendNode(fragment);
|
||||
|
||||
assert.equal(
|
||||
drupalHtmlBuilder.build(),
|
||||
`<style type="text/css">:root .sections > h2 { background: red}</style>
|
||||
<script>let x = 10;
|
||||
let y = 5;
|
||||
if (y < x) {
|
||||
console.log('is smaller')
|
||||
}</script>`,
|
||||
);
|
||||
},
|
||||
'should return correct HTML from fragment with comment': function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
const fragment = document.createDocumentFragment();
|
||||
const div = document.createElement('div');
|
||||
const comment = document.createComment('bar');
|
||||
div.textContent = 'bar';
|
||||
|
||||
fragment.appendChild(div);
|
||||
fragment.appendChild(comment);
|
||||
|
||||
drupalHtmlBuilder.appendNode(fragment);
|
||||
|
||||
assert.equal(drupalHtmlBuilder.build(), '<div>bar</div><!--bar-->');
|
||||
},
|
||||
'should return correct HTML from fragment with attributes': function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
const fragment = document.createDocumentFragment();
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('id', 'foo');
|
||||
div.classList.add('bar');
|
||||
div.textContent = 'baz';
|
||||
|
||||
fragment.appendChild(div);
|
||||
drupalHtmlBuilder.appendNode(fragment);
|
||||
|
||||
assert.equal(
|
||||
drupalHtmlBuilder.build(),
|
||||
'<div id="foo" class="bar">baz</div>',
|
||||
);
|
||||
},
|
||||
'should return correct HTML from fragment with self closing tag':
|
||||
function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
const fragment = document.createDocumentFragment();
|
||||
const hr = document.createElement('hr');
|
||||
|
||||
fragment.appendChild(hr);
|
||||
drupalHtmlBuilder.appendNode(fragment);
|
||||
|
||||
assert.equal(drupalHtmlBuilder.build(), '<hr>');
|
||||
},
|
||||
'attribute values should be escaped': function () {
|
||||
const drupalHtmlBuilder = new DrupalHtmlBuilder();
|
||||
const fragment = document.createDocumentFragment();
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('data-caption', 'Kittens & llamas are <em>cute</em>');
|
||||
div.textContent = 'foo';
|
||||
|
||||
fragment.appendChild(div);
|
||||
drupalHtmlBuilder.appendNode(fragment);
|
||||
|
||||
assert.equal(
|
||||
drupalHtmlBuilder.build(),
|
||||
'<div data-caption="Kittens & llamas are <em>cute</em>">foo</div>',
|
||||
);
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Traits;
|
||||
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Drupal\Component\Utility\Html;
|
||||
|
||||
// cspell:ignore downcasted
|
||||
|
||||
/**
|
||||
* Provides methods to test CKEditor 5.
|
||||
*
|
||||
* This trait is meant to be used only by functional JavaScript test classes.
|
||||
*/
|
||||
trait CKEditor5TestTrait {
|
||||
|
||||
/**
|
||||
* Gets CKEditor 5 instance data as a PHP DOMDocument.
|
||||
*
|
||||
* @return \DOMDocument
|
||||
* The result of parsing CKEditor 5's data into a PHP DOMDocument.
|
||||
*/
|
||||
protected function getEditorDataAsDom(): \DOMDocument {
|
||||
return Html::load($this->getEditorDataAsHtmlString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets CKEditor 5 instance data as a HTML string.
|
||||
*
|
||||
* @return string
|
||||
* The result of retrieving CKEditor 5's data.
|
||||
*
|
||||
* @see https://ckeditor.com/docs/ckeditor5/latest/api/module_editor-classic_classiceditor-ClassicEditor.html#function-getData
|
||||
*/
|
||||
protected function getEditorDataAsHtmlString(): string {
|
||||
// We cannot trust on CKEditor updating the textarea every time model
|
||||
// changes. Therefore, the most reliable way to get downcasted data is to
|
||||
// use the CKEditor API.
|
||||
$javascript = <<<JS
|
||||
(function(){
|
||||
return Drupal.CKEditor5Instances.get(Drupal.CKEditor5Instances.keys().next().value).getData();
|
||||
})();
|
||||
JS;
|
||||
return $this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for CKEditor to initialize.
|
||||
*/
|
||||
protected function waitForEditor(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a CKEditor button.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the button, such as `drupallink`, `source`, etc.
|
||||
*/
|
||||
protected function pressEditorButton($name): void {
|
||||
$this->getEditorButton($name)->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a CKEditor button and returns it when available and visible.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the button, such as `drupallink`, `source`, etc.
|
||||
*
|
||||
* @return \Behat\Mink\Element\NodeElement|null
|
||||
* The page element node if found, NULL if not.
|
||||
*/
|
||||
protected function getEditorButton($name) {
|
||||
$button = $this->assertSession()->waitForElementVisible('xpath', "//button[span[text()='$name']]");
|
||||
$this->assertNotEmpty($button);
|
||||
return $button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a CKEditor button is disabled.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the button, such as `drupallink`, `source`, etc.
|
||||
*/
|
||||
protected function assertEditorButtonDisabled($name): void {
|
||||
$button = $this->getEditorButton($name);
|
||||
$this->assertTrue($button->hasAttribute('aria-disabled'));
|
||||
$this->assertTrue($button->hasClass('ck-disabled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a CKEditor button is enabled.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the button, such as `drupallink`, `source`, etc.
|
||||
*/
|
||||
protected function assertEditorButtonEnabled($name): void {
|
||||
$button = $this->getEditorButton($name);
|
||||
$this->assertFalse($button->hasAttribute('aria-disabled'));
|
||||
$this->assertFalse($button->hasClass('ck-disabled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a particular balloon is visible.
|
||||
*
|
||||
* @param string $balloon_content_selector
|
||||
* A CSS selector.
|
||||
*
|
||||
* @return \Behat\Mink\Element\NodeElement
|
||||
* The asserted balloon.
|
||||
*/
|
||||
protected function assertVisibleBalloon(string $balloon_content_selector): NodeElement {
|
||||
$this->assertSession()->elementExists('css', '.ck-balloon-panel_visible');
|
||||
$selector = ".ck-balloon-panel_visible .ck-balloon-rotator__content > .ck$balloon_content_selector";
|
||||
$this->assertSession()->elementExists('css', $selector);
|
||||
return $this->getSession()->getPage()->find('css', $selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a button from the currently visible balloon.
|
||||
*
|
||||
* @param string $name
|
||||
* The label of the button to find.
|
||||
*
|
||||
* @return \Behat\Mink\Element\NodeElement
|
||||
* The requested button.
|
||||
*/
|
||||
protected function getBalloonButton(string $name): NodeElement {
|
||||
$button = $this->getSession()->getPage()
|
||||
->find('css', '.ck-balloon-panel_visible .ck-balloon-rotator__content')
|
||||
->find('xpath', "//button[span[text()='$name']]");
|
||||
$this->assertNotEmpty($button);
|
||||
return $button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects text inside an element.
|
||||
*
|
||||
* @param string $selector
|
||||
* A CSS selector for the element which contents should be selected.
|
||||
*/
|
||||
protected function selectTextInsideElement(string $selector): void {
|
||||
$javascript = <<<JS
|
||||
(function() {
|
||||
const el = document.querySelector(".ck-editor__main $selector");
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
})();
|
||||
JS;
|
||||
$this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Traits;
|
||||
|
||||
/**
|
||||
* Provides methods to test protected/private methods in unit tests.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait PrivateMethodUnitTestTrait {
|
||||
|
||||
/**
|
||||
* Gets a protected/private method to test.
|
||||
*
|
||||
* @param string $fqcn
|
||||
* A fully qualified classname.
|
||||
* @param string $name
|
||||
* The method name.
|
||||
*
|
||||
* @return \ReflectionMethod
|
||||
* The accessible method.
|
||||
*/
|
||||
protected static function getMethod(string $fqcn, string $name): \ReflectionMethod {
|
||||
$class = new \ReflectionClass($fqcn);
|
||||
$method = $class->getMethod($name);
|
||||
return $method;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Traits;
|
||||
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Synchronizes the child site's CSRF token seed back to the test runner.
|
||||
*
|
||||
* For the test to be able to generate valid CSRF tokens, it needs access to the
|
||||
* CSRF token seed in the child site (i.e. tested site). This requires reading
|
||||
* the CSRF token seed from the session that gets created in the child site
|
||||
* after logging in, and then setting it in the test runner's container.
|
||||
* Otherwise, the test runner would generate its own CSRF token seed and would
|
||||
* hence generate CSRF tokens that are not valid for the session in the child
|
||||
* site.
|
||||
*
|
||||
* @see \Drupal\Core\Access\CsrfTokenGenerator::get()
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait SynchronizeCsrfTokenSeedTrait {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function drupalLogin(AccountInterface $account) {
|
||||
parent::drupalLogin($account);
|
||||
$session_data = $this->container->get('session_handler.write_safe')->read($this->getSession()->getCookie($this->getSessionName()));
|
||||
$csrf_token_seed = unserialize(explode('_sf2_meta|', $session_data)[1])['s'];
|
||||
$this->container->get('session_manager.metadata_bag')->setCsrfTokenSeed($csrf_token_seed);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function rebuildContainer() {
|
||||
parent::rebuildContainer();
|
||||
|
||||
// Ensure that the CSRF token seed is reset on container rebuild.
|
||||
if ($this->loggedInUser) {
|
||||
$current_user = $this->loggedInUser;
|
||||
$this->drupalLogout();
|
||||
$this->drupalLogin($current_user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function drupalLogout() {
|
||||
parent::drupalLogout();
|
||||
$this->container->get('session_manager.metadata_bag')->stampNew();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Unit;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Alignment;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Alignment
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class AlignmentPluginTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Provides a list of configs to test.
|
||||
*/
|
||||
public static function providerGetDynamicPluginConfig(): array {
|
||||
return [
|
||||
'All alignments' => [
|
||||
Alignment::DEFAULT_CONFIGURATION,
|
||||
[
|
||||
'alignment' => [
|
||||
'options' => [
|
||||
[
|
||||
'name' => 'left',
|
||||
'className' => 'text-align-left',
|
||||
],
|
||||
[
|
||||
'name' => 'center',
|
||||
'className' => 'text-align-center',
|
||||
],
|
||||
[
|
||||
'name' => 'right',
|
||||
'className' => 'text-align-right',
|
||||
],
|
||||
[
|
||||
'name' => 'justify',
|
||||
'className' => 'text-align-justify',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'No alignments allowed' => [
|
||||
[
|
||||
'enabled_alignments' => [],
|
||||
],
|
||||
[
|
||||
'alignment' => [
|
||||
'options' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Left only' => [
|
||||
[
|
||||
'enabled_alignments' => [
|
||||
'left',
|
||||
],
|
||||
],
|
||||
[
|
||||
'alignment' => [
|
||||
'options' => [
|
||||
[
|
||||
'name' => 'left',
|
||||
'className' => 'text-align-left',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Left and justify only' => [
|
||||
[
|
||||
'enabled_alignments' => [
|
||||
'left',
|
||||
'justify',
|
||||
],
|
||||
],
|
||||
[
|
||||
'alignment' => [
|
||||
'options' => [
|
||||
[
|
||||
'name' => 'left',
|
||||
'className' => 'text-align-left',
|
||||
],
|
||||
[
|
||||
'name' => 'justify',
|
||||
'className' => 'text-align-justify',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getDynamicPluginConfig
|
||||
* @dataProvider providerGetDynamicPluginConfig
|
||||
*/
|
||||
public function testGetDynamicPluginConfig(array $configuration, array $expected_dynamic_config): void {
|
||||
// Read the CKEditor 5 plugin's static configuration from YAML.
|
||||
$ckeditor5_plugin_definitions = Yaml::parseFile(__DIR__ . '/../../../ckeditor5.ckeditor5.yml');
|
||||
$static_plugin_config = $ckeditor5_plugin_definitions['ckeditor5_alignment']['ckeditor5']['config'];
|
||||
|
||||
$plugin = new Alignment($configuration, 'ckeditor5_alignment', NULL);
|
||||
$dynamic_plugin_config = $plugin->getDynamicPluginConfig($static_plugin_config, $this->prophesize(EditorInterface::class)
|
||||
->reveal());
|
||||
|
||||
$this->assertSame($expected_dynamic_config, $dynamic_plugin_config);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Unit;
|
||||
|
||||
use Drupal\ckeditor5\Controller\CKEditor5ImageController;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface;
|
||||
use Drupal\Core\Entity\EntityConstraintViolationList;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use Drupal\Core\Lock\LockBackendInterface;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\file\FileInterface;
|
||||
use Drupal\file\Upload\FileUploadHandlerInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/**
|
||||
* Tests CKEditor5ImageController.
|
||||
*
|
||||
* @group ckeditor5
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Controller\CKEditor5ImageController
|
||||
*/
|
||||
final class CKEditor5ImageControllerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Tests that upload fails correctly when the file is too large.
|
||||
*/
|
||||
public function testInvalidFile(): void {
|
||||
$file_system = $this->prophesize(FileSystemInterface::class);
|
||||
$file_system->move(Argument::any())->shouldNotBeCalled();
|
||||
$directory = 'public://';
|
||||
$file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)->willReturn(TRUE);
|
||||
$file_system->getDestinationFilename(Argument::cetera())->willReturn('/tmp/foo.txt');
|
||||
$lock = $this->prophesize(LockBackendInterface::class);
|
||||
$lock->acquire(Argument::any())->willReturn(TRUE);
|
||||
$container = $this->prophesize(ContainerInterface::class);
|
||||
$file_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
$file = $this->prophesize(FileInterface::class);
|
||||
$violations = $this->prophesize(EntityConstraintViolationList::class);
|
||||
$violations->count()->willReturn(0);
|
||||
$file->validate()->willReturn($violations->reveal());
|
||||
$file_storage->create(Argument::any())->willReturn($file->reveal());
|
||||
$entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
|
||||
$entity_type_manager->getStorage('file')->willReturn($file_storage->reveal());
|
||||
$container->get('entity_type.manager')->willReturn($entity_type_manager->reveal());
|
||||
$entity_type_repository = $this->prophesize(EntityTypeRepositoryInterface::class);
|
||||
$entity_type_repository->getEntityTypeFromClass(File::class)->willReturn('file');
|
||||
$container->get('entity_type.repository')->willReturn($entity_type_repository->reveal());
|
||||
\Drupal::setContainer($container->reveal());
|
||||
$controller = new CKEditor5ImageController(
|
||||
$file_system->reveal(),
|
||||
$this->prophesize(FileUploadHandlerInterface::class)->reveal(),
|
||||
$lock->reveal(),
|
||||
$this->prophesize(CKEditor5PluginManagerInterface::class)->reveal(),
|
||||
);
|
||||
// We can't use vfsstream here because of how Symfony request works.
|
||||
$file_uri = tempnam(sys_get_temp_dir(), 'tmp');
|
||||
$fp = fopen($file_uri, 'w');
|
||||
fwrite($fp, 'foo');
|
||||
fclose($fp);
|
||||
$request = Request::create('/', files: [
|
||||
'upload' => [
|
||||
'name' => 'foo.txt',
|
||||
'type' => 'text/plain',
|
||||
'size' => 42,
|
||||
'tmp_name' => $file_uri,
|
||||
'error' => \UPLOAD_ERR_FORM_SIZE,
|
||||
],
|
||||
]);
|
||||
$editor = $this->prophesize(EditorInterface::class);
|
||||
$request->attributes->set('editor', $editor->reveal());
|
||||
$this->expectException(HttpException::class);
|
||||
$this->expectExceptionMessage('The file "foo.txt" exceeds the upload limit defined in your form.');
|
||||
$controller->upload($request);
|
||||
}
|
||||
|
||||
}
|
||||
104
web/core/modules/ckeditor5/tests/src/Unit/CKEditor5Test.php
Normal file
104
web/core/modules/ckeditor5/tests/src/Unit/CKEditor5Test.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Unit;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\Tests\ckeditor5\Traits\PrivateMethodUnitTestTrait;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\Editor\CKEditor5
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class CKEditor5Test extends UnitTestCase {
|
||||
|
||||
use PrivateMethodUnitTestTrait;
|
||||
|
||||
/**
|
||||
* Simulated CKEditor5::buildConfigurationForm() form structure.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected const SIMULATED_FORM_STRUCTURE = [
|
||||
'toolbar' => [
|
||||
'available' => [],
|
||||
'items' => [],
|
||||
],
|
||||
'available_items_description' => [],
|
||||
'active_items_description' => [],
|
||||
'plugin_settings' => [],
|
||||
'plugins' => [
|
||||
'providerA_plugin1' => [],
|
||||
'providerB_plugin2' => [
|
||||
'foo' => [],
|
||||
'bar' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @covers \Drupal\ckeditor5\Plugin\Editor\CKEditor5::mapViolationPropertyPathsToFormNames
|
||||
* @dataProvider providerPathsToFormNames
|
||||
*/
|
||||
public function testPathsToFormNames(string $property_path, string $expected_form_item_name, bool $expect_exception = FALSE): void {
|
||||
$mapMethod = self::getMethod(CKEditor5::class, 'mapViolationPropertyPathsToFormNames');
|
||||
if ($expect_exception) {
|
||||
$this->expectExceptionMessage('assert($shifted === \'settings\')');
|
||||
}
|
||||
|
||||
$form_item_name = $mapMethod->invokeArgs(NULL, [$property_path, static::SIMULATED_FORM_STRUCTURE]);
|
||||
|
||||
if (!$expect_exception) {
|
||||
$this->assertSame($expected_form_item_name, $form_item_name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testing mapViolationPropertyPathsToFormNames.
|
||||
*
|
||||
* @return array[]
|
||||
* An array with the property path and expected form item name.
|
||||
*/
|
||||
public static function providerPathsToFormNames(): array {
|
||||
return [
|
||||
'validation error targeting toolbar items' => [
|
||||
'settings.toolbar.items',
|
||||
'settings][toolbar][items',
|
||||
],
|
||||
'validation error targeting a specific toolbar item' => [
|
||||
'settings.toolbar.items.6',
|
||||
'settings][toolbar][items',
|
||||
],
|
||||
'validation error targeting a simple plugin form' => [
|
||||
'settings.plugins.providerA_plugin1',
|
||||
'settings][plugins][providerA_plugin1',
|
||||
],
|
||||
'validation error targeting a simple plugin form, with deep config schema detail' => [
|
||||
'settings.plugins.providerA_plugin1.foo.bar.baz',
|
||||
'settings][plugins][providerA_plugin1',
|
||||
],
|
||||
'validation error targeting a complex plugin form' => [
|
||||
'settings.plugins.providerB_plugin2',
|
||||
'settings][plugins][providerB_plugin2',
|
||||
],
|
||||
'validation error targeting a complex plugin form, with deep config schema detail' => [
|
||||
'settings.plugins.providerB_plugin2.foo.bar.baz',
|
||||
'settings][plugins][providerB_plugin2][foo',
|
||||
],
|
||||
'unrealistic example one — should trigger exception' => [
|
||||
'bad.bad.worst',
|
||||
'I DO NOT EXIST',
|
||||
TRUE,
|
||||
],
|
||||
'unrealistic example two — should trigger exception' => [
|
||||
'one.two.three.four',
|
||||
'one][two][three][four',
|
||||
TRUE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
1756
web/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php
Normal file
1756
web/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php
Normal file
File diff suppressed because it is too large
Load Diff
120
web/core/modules/ckeditor5/tests/src/Unit/HeadingPluginTest.php
Normal file
120
web/core/modules/ckeditor5/tests/src/Unit/HeadingPluginTest.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Unit;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Language
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class HeadingPluginTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Provides a list of configs to test.
|
||||
*/
|
||||
public static function providerGetDynamicPluginConfig(): array {
|
||||
// Prepare headings matching ckeditor5.ckeditor5.yml to also protect
|
||||
// against unexpected changes to the YAML file given the YAML file is used
|
||||
// to generate the dynamic plugin configuration.
|
||||
$paragraph = [
|
||||
'model' => 'paragraph',
|
||||
'title' => 'Paragraph',
|
||||
'class' => 'ck-heading_paragraph',
|
||||
];
|
||||
$headings = [];
|
||||
foreach (range(2, 6) as $number) {
|
||||
$headings[$number] = [
|
||||
'model' => 'heading' . $number,
|
||||
'view' => 'h' . $number,
|
||||
'title' => 'Heading ' . $number,
|
||||
'class' => 'ck-heading_heading' . $number,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'All headings' => [
|
||||
Heading::DEFAULT_CONFIGURATION,
|
||||
[
|
||||
'heading' => [
|
||||
'options' => [
|
||||
$paragraph,
|
||||
$headings[2],
|
||||
$headings[3],
|
||||
$headings[4],
|
||||
$headings[5],
|
||||
$headings[6],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Only required headings' => [
|
||||
[
|
||||
'enabled_headings' => [],
|
||||
],
|
||||
[
|
||||
'heading' => [
|
||||
'options' => [
|
||||
$paragraph,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Heading 2 only' => [
|
||||
[
|
||||
'enabled_headings' => [
|
||||
'heading2',
|
||||
],
|
||||
],
|
||||
[
|
||||
'heading' => [
|
||||
'options' => [
|
||||
$paragraph,
|
||||
$headings[2],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Heading 2 and 3 only' => [
|
||||
[
|
||||
'enabled_headings' => [
|
||||
'heading2',
|
||||
'heading3',
|
||||
],
|
||||
],
|
||||
[
|
||||
'heading' => [
|
||||
'options' => [
|
||||
$paragraph,
|
||||
$headings[2],
|
||||
$headings[3],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getDynamicPluginConfig
|
||||
* @dataProvider providerGetDynamicPluginConfig
|
||||
*/
|
||||
public function testGetDynamicPluginConfig(array $configuration, array $expected_dynamic_config): void {
|
||||
// Read the CKEditor 5 plugin's static configuration from YAML.
|
||||
$ckeditor5_plugin_definitions = Yaml::parseFile(__DIR__ . '/../../../ckeditor5.ckeditor5.yml');
|
||||
$static_plugin_config = $ckeditor5_plugin_definitions['ckeditor5_heading']['ckeditor5']['config'];
|
||||
|
||||
$plugin = new Heading($configuration, 'ckeditor5_heading', NULL);
|
||||
$dynamic_plugin_config = $plugin->getDynamicPluginConfig($static_plugin_config, $this->prophesize(EditorInterface::class)
|
||||
->reveal());
|
||||
|
||||
$this->assertSame($expected_dynamic_config, $dynamic_plugin_config);
|
||||
}
|
||||
|
||||
}
|
||||
145
web/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php
Normal file
145
web/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Unit;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Language;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
|
||||
use Drupal\Core\Language\Language as LanguageLanguage;
|
||||
use Drupal\Core\Language\LanguageManager;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Routing\RouteProviderInterface;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Language
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class LanguagePluginTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Provides a list of configs to test.
|
||||
*/
|
||||
public static function providerGetDynamicPluginConfig(): array {
|
||||
$united_nations_expected_output = [
|
||||
'language' => [
|
||||
'textPartLanguage' => [
|
||||
[
|
||||
'title' => 'Arabic',
|
||||
'languageCode' => 'ar',
|
||||
'textDirection' => 'rtl',
|
||||
],
|
||||
[
|
||||
'title' => 'Chinese, Simplified',
|
||||
'languageCode' => 'zh-hans',
|
||||
],
|
||||
[
|
||||
'title' => 'English',
|
||||
'languageCode' => 'en',
|
||||
],
|
||||
[
|
||||
'title' => 'French',
|
||||
'languageCode' => 'fr',
|
||||
],
|
||||
[
|
||||
'title' => 'Russian',
|
||||
'languageCode' => 'ru',
|
||||
],
|
||||
[
|
||||
'title' => 'Spanish',
|
||||
'languageCode' => 'es',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
return [
|
||||
'un' => [
|
||||
['language_list' => 'un'],
|
||||
$united_nations_expected_output,
|
||||
],
|
||||
'site_configured' => [
|
||||
['language_list' => 'site_configured'],
|
||||
[
|
||||
'language' => [
|
||||
'textPartLanguage' => [
|
||||
[
|
||||
'title' => 'Arabic',
|
||||
'languageCode' => 'ar',
|
||||
'textDirection' => 'rtl',
|
||||
],
|
||||
[
|
||||
'title' => 'German',
|
||||
'languageCode' => 'de',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'all' => [
|
||||
['language_list' => 'all'],
|
||||
[
|
||||
'language' => [
|
||||
'textPartLanguage' => static::buildExpectedDynamicConfig(LanguageManager::getStandardLanguageList()),
|
||||
],
|
||||
],
|
||||
],
|
||||
'default configuration' => [
|
||||
[],
|
||||
$united_nations_expected_output,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the expected dynamic configuration output given a language list.
|
||||
*
|
||||
* @param array $language_list
|
||||
* The languages list from the language manager.
|
||||
*
|
||||
* @return array
|
||||
* The expected output of the dynamic plugin configuration.
|
||||
*/
|
||||
protected static function buildExpectedDynamicConfig(array $language_list): array {
|
||||
$expected_language_config = [];
|
||||
foreach ($language_list as $language_code => $language_list_item) {
|
||||
$item = [
|
||||
'title' => $language_list_item[0],
|
||||
'languageCode' => $language_code,
|
||||
];
|
||||
if (isset($language_list_item[2])) {
|
||||
$item['textDirection'] = $language_list_item[2];
|
||||
}
|
||||
$expected_language_config[$item['title']] = $item;
|
||||
}
|
||||
ksort($expected_language_config);
|
||||
return array_values($expected_language_config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getDynamicPluginConfig
|
||||
* @dataProvider providerGetDynamicPluginConfig
|
||||
*/
|
||||
public function testGetDynamicPluginConfig(array $configuration, array $expected_dynamic_config): void {
|
||||
$route_provider = $this->prophesize(RouteProviderInterface::class);
|
||||
$language_manager = $this->prophesize(LanguageManagerInterface::class);
|
||||
$language_manager->getLanguages()->willReturn([
|
||||
new LanguageLanguage([
|
||||
'id' => 'de',
|
||||
'name' => 'German',
|
||||
]),
|
||||
new LanguageLanguage([
|
||||
'id' => 'ar',
|
||||
'name' => 'Arabic',
|
||||
'direction' => 'rtl',
|
||||
]),
|
||||
]);
|
||||
$plugin = new Language($configuration, 'ckeditor5_language', new CKEditor5PluginDefinition(['id' => 'IRRELEVANT-FOR-A-UNIT-TEST']), $language_manager->reveal(), $route_provider->reveal());
|
||||
$dynamic_config = $plugin->getDynamicPluginConfig([], $this->prophesize(EditorInterface::class)
|
||||
->reveal());
|
||||
$this->assertSame($expected_dynamic_config, $dynamic_config);
|
||||
}
|
||||
|
||||
}
|
||||
118
web/core/modules/ckeditor5/tests/src/Unit/ListPluginTest.php
Normal file
118
web/core/modules/ckeditor5/tests/src/Unit/ListPluginTest.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Unit;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\ListPlugin;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ListPlugin
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class ListPluginTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Provides a list of configs to test.
|
||||
*/
|
||||
public static function providerGetDynamicPluginConfig(): array {
|
||||
return [
|
||||
'startIndex is false' => [
|
||||
[
|
||||
'properties' => [
|
||||
'reversed' => TRUE,
|
||||
'startIndex' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
[
|
||||
'list' => [
|
||||
'properties' => [
|
||||
'reversed' => TRUE,
|
||||
'startIndex' => FALSE,
|
||||
'styles' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'reversed is false' => [
|
||||
[
|
||||
'properties' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => TRUE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
[
|
||||
'list' => [
|
||||
'properties' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => TRUE,
|
||||
'styles' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'both disabled' => [
|
||||
[
|
||||
'properties' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
[
|
||||
'list' => [
|
||||
'properties' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => FALSE,
|
||||
'styles' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'both enabled' => [
|
||||
[
|
||||
'properties' => [
|
||||
'reversed' => TRUE,
|
||||
'startIndex' => TRUE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
[
|
||||
'list' => [
|
||||
'properties' => [
|
||||
'reversed' => TRUE,
|
||||
'startIndex' => TRUE,
|
||||
'styles' => FALSE,
|
||||
],
|
||||
'multiBlock' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getDynamicPluginConfig
|
||||
*
|
||||
* @dataProvider providerGetDynamicPluginConfig
|
||||
*/
|
||||
public function testGetDynamicPluginConfig(array $configuration, array $expected_dynamic_config): void {
|
||||
// Read the CKEditor 5 plugin's static configuration from YAML.
|
||||
$ckeditor5_plugin_definitions = Yaml::parseFile(__DIR__ . '/../../../ckeditor5.ckeditor5.yml');
|
||||
$static_plugin_config = $ckeditor5_plugin_definitions['ckeditor5_list']['ckeditor5']['config'];
|
||||
$plugin = new ListPlugin($configuration, 'ckeditor5_list', NULL);
|
||||
$dynamic_plugin_config = $plugin->getDynamicPluginConfig($static_plugin_config, $this->prophesize(Editor::class)
|
||||
->reveal());
|
||||
$this->assertSame($expected_dynamic_config, $dynamic_plugin_config);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user