MDL-75982 editor_tiny: Add behat tests
This commit starts to add Behat tests for the various features of TinyMCE. Please note that not all features are testable with Behat.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
@editor @editor_tiny @tiny_media @javascript
|
||||
Feature: Use the TinyMCE editor to upload an image
|
||||
In order to work with images
|
||||
As a user
|
||||
I need to be able to upload and manipulate images
|
||||
|
||||
Scenario: Clicking on the Image button in the TinyMCE editor opens the image dialog
|
||||
Given I log in as "admin"
|
||||
And I open my profile in edit mode
|
||||
When I click on the "Image" button for the "Description" TinyMCE editor
|
||||
Then "Image properties" "dialogue" should exist
|
||||
|
||||
Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker
|
||||
Given I log in as "admin"
|
||||
And I open my profile in edit mode
|
||||
When I click on the "Image" button for the "Description" TinyMCE editor
|
||||
And I click on "Browse repositories" "button" in the "Image properties" "dialogue"
|
||||
Then "File picker" "dialogue" should exist
|
||||
|
||||
@_file_upload @test_tiny
|
||||
Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker
|
||||
Given I log in as "admin"
|
||||
And I open my profile in edit mode
|
||||
When I click on the "Image" button for the "Description" TinyMCE editor
|
||||
And I click on "Browse repositories" "button" in the "Image properties" "dialogue"
|
||||
And I upload "/lib/editor/tiny/tests/behat/fixtures/tinyscreenshot.png" to the file picker for TinyMCE
|
||||
# Note: This needs to be replaced with a label.
|
||||
Then ".tiny_image_preview" "css_element" should be visible
|
||||
@@ -24,6 +24,9 @@
|
||||
*/
|
||||
|
||||
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\DriverException;
|
||||
use Behat\Mink\Exception\ExpectationException;
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
require_once(__DIR__ . '/../../../../behat/behat_base.php');
|
||||
@@ -36,6 +39,52 @@ require_once(__DIR__ . '/../../../../behat/behat_base.php');
|
||||
* @copyright 2022 Andrew Lyons <[email protected]>
|
||||
*/
|
||||
class behat_editor_tiny extends behat_base implements \core_behat\settable_editor {
|
||||
/**
|
||||
* Execute some JavaScript for a particular Editor instance.
|
||||
*
|
||||
* The editor instance is available on the 'instnace' variable.
|
||||
*
|
||||
* @param string $editorid The ID of the editor
|
||||
* @param string $code The code to execute
|
||||
*/
|
||||
protected function execute_javascript_for_editor(string $editorid, string $code): void {
|
||||
$js = <<<EOF
|
||||
require(['editor_tiny/editor'], (editor) => {
|
||||
const instance = editor.getInstanceForElementId('${editorid}');
|
||||
{$code}
|
||||
});
|
||||
EOF;
|
||||
|
||||
$this->execute_script($js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve some JavaScript for a particular Editor instance.
|
||||
*
|
||||
* The editor instance is available on the 'instnace' variable.
|
||||
* The code should return a value by passing it to the `resolve` function.
|
||||
*
|
||||
* @param string $editorid The ID of the editor
|
||||
* @param string $code The code to evaluate
|
||||
* @return string|null|array
|
||||
*/
|
||||
protected function evaluate_javascript_for_editor(string $editorid, string $code) {
|
||||
$js = <<<EOF
|
||||
return new Promise((resolve, reject) => {
|
||||
require(['editor_tiny/editor'], (editor) => {
|
||||
const instance = editor.getInstanceForElementId('${editorid}');
|
||||
if (!instance) {
|
||||
reject("Instance '${editorid}' not found");
|
||||
}
|
||||
|
||||
{$code}
|
||||
});
|
||||
});
|
||||
EOF;
|
||||
|
||||
return $this->evaluate_script($js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the editor.
|
||||
*
|
||||
@@ -51,17 +100,19 @@ class behat_editor_tiny extends behat_base implements \core_behat\settable_edito
|
||||
return;
|
||||
}
|
||||
|
||||
$js = <<<EOF
|
||||
require(['editor_tiny/editor'], (editor) => {
|
||||
const instance = editor.getInstanceForElementId('${editorid}');
|
||||
if (instance) {
|
||||
instance.setContent('${value}');
|
||||
instance.undoManager.add();
|
||||
}
|
||||
});
|
||||
EOF;
|
||||
$this->execute_javascript_for_editor($editorid, <<<EOF
|
||||
instance.setContent('${value}');
|
||||
instance.undoManager.add();
|
||||
EOF);
|
||||
}
|
||||
|
||||
$this->execute_script($js);
|
||||
/**
|
||||
* Store the current value of the editor, if it is a Tiny editor, to the textarea.
|
||||
*
|
||||
* @param string $editorid The ID of the editor.
|
||||
*/
|
||||
public function store_current_value(string $editorid): void {
|
||||
$this->execute_javascript_for_editor($editorid, "instance?.save();");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,4 +143,322 @@ class behat_editor_tiny extends behat_base implements \core_behat\settable_edito
|
||||
|
||||
$this->execute('behat_general::the_default_editor_is_set_to', ['tiny']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the editor_tiny tag is in use.
|
||||
*
|
||||
* This function should be used for any step defined in this file.
|
||||
*
|
||||
* @throws DriverException Thrown if the editor_tiny tag is not specified for this file
|
||||
*/
|
||||
protected function require_tiny_tags(): void {
|
||||
// Ensure that this step only runs in TinyMCE tags.
|
||||
if (!$this->has_tag('editor_tiny')) {
|
||||
throw new DriverException(
|
||||
'TinyMCE tests using this step must have the @editor_tiny tag on either the scenario or feature.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Mink NodeElement of the <textarea> for the specified locator.
|
||||
*
|
||||
* Moodle mostly referes to the textarea, rather than the editor itself and interactions are translated to the
|
||||
* Editor using the TinyMCE API.
|
||||
*
|
||||
* @param string $locator A Moodle field locator
|
||||
* @return NodeElement The element found by the find_field function
|
||||
*/
|
||||
protected function get_textarea_for_locator(string $locator): NodeElement {
|
||||
return $this->find_field($locator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Mink NodeElement of the container for the specified locator.
|
||||
*
|
||||
* This is the top-most HTML element for the editor found by TinyMCE.getContainer().
|
||||
*
|
||||
* @param string $locator A Moodle field locator
|
||||
* @return NodeElement The Mink NodeElement representing the container.
|
||||
*/
|
||||
protected function get_editor_container_for_locator(string $locator): NodeElement {
|
||||
$textarea = $this->get_textarea_for_locator($locator);
|
||||
$editorid = $textarea->getAttribute('id');
|
||||
|
||||
$targetid = uniqid();
|
||||
$js = <<<EOF
|
||||
const container = instance.getContainer();
|
||||
if (!container.id) {
|
||||
container.id = '${targetid}';
|
||||
}
|
||||
resolve(container.id);
|
||||
EOF;
|
||||
$containerid = $this->evaluate_javascript_for_editor($editorid, $js);
|
||||
|
||||
return $this->find('css', "#{$containerid}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the iframe relating to the editor.
|
||||
*
|
||||
* If no name is found, then add one.
|
||||
*
|
||||
* If the editor it not found, then throw an exception.
|
||||
*
|
||||
* @param string $locator The name of the editor
|
||||
* @return string The name of the iframe
|
||||
*/
|
||||
protected function get_editor_iframe_name(string $locator): string {
|
||||
return $this->get_editor_iframe_name_for_element($this->get_textarea_for_locator($locator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the iframe relating to the editor.
|
||||
*
|
||||
* If no name is found, then add one.
|
||||
*
|
||||
* If the editor it not found, then throw an exception.
|
||||
|
||||
* @param NodeElement $editor The editor element
|
||||
* @return string The name of the iframe
|
||||
*/
|
||||
protected function get_editor_iframe_name_for_element(NodeElement $editor): string {
|
||||
$editorid = $editor->getAttribute('id');
|
||||
|
||||
// Ensure that a name is set on the iframe relating to the editorid.
|
||||
$js = <<<EOF
|
||||
if (!instance.iframeElement.name) {
|
||||
instance.iframeElement.name = '${editorid}';
|
||||
}
|
||||
resolve(instance.iframeElement.name);
|
||||
EOF;
|
||||
|
||||
return $this->evaluate_javascript_for_editor($editorid, $js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a button for the specified TinyMCE editor.
|
||||
*
|
||||
* @When /^I click on the "(?P<button_string>(?:[^"]|\\")*)" button for the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor$/
|
||||
*
|
||||
* @param string $button The label of the button
|
||||
* @param string $locator The locator for the editor
|
||||
*/
|
||||
public function i_click_on_button(string $button, string $locator): void {
|
||||
$this->require_tiny_tags();
|
||||
$container = $this->get_editor_container_for_locator($locator);
|
||||
|
||||
$this->execute('behat_general::i_click_on_in_the', [$button, 'button', $container, 'NodeElement']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm that the button state of the specified button/editor combination matches the expectation.
|
||||
*
|
||||
* @Then /^the "(?P<button_string>(?:[^"]|\\")*)" button of the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor has state "(?P<state_string>(?:[^"]|\\")*)"$/
|
||||
*
|
||||
* @param string $button The text name of the button
|
||||
* @param string $locator The locator string for the editor
|
||||
* @param string $state The state of the button
|
||||
* @throws ExpectationException Thrown if the button state is not correct
|
||||
*/
|
||||
public function button_state_is(string $button, string $locator, string $state): void {
|
||||
$this->require_tiny_tags();
|
||||
$container = $this->get_editor_container_for_locator($locator);
|
||||
|
||||
$button = $this->find_button($button, false, $container);
|
||||
$buttonstate = $button->getAttribute('aria-pressed');
|
||||
|
||||
if ($buttonstate !== $state) {
|
||||
throw new ExpectationException("Button '{$button}' is in state '{$buttonstate}' not '{$state}'", $this->getSession());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a button for the specified TinyMCE editor.
|
||||
*
|
||||
* @When /^I click on the "(?P<menuitem_string>(?:[^"]|\\")*)" menu item for the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor$/
|
||||
*
|
||||
* @param string $menuitem The label of the menu item
|
||||
* @param string $locator The locator for the editor
|
||||
*/
|
||||
public function i_click_on_menuitem_in_menu(string $menuitem, string $locator): void {
|
||||
$this->require_tiny_tags();
|
||||
$container = $this->get_editor_container_for_locator($locator);
|
||||
|
||||
$menubar = $container->find('css', '[role="menubar"]');
|
||||
|
||||
$menus = array_map(function(string $value): string {
|
||||
return trim($value);
|
||||
}, explode('>', $menuitem));
|
||||
|
||||
// Open the menu bar.
|
||||
$mainmenu = array_shift($menus);
|
||||
$this->execute('behat_general::i_click_on_in_the', [$mainmenu, 'button', $menubar, 'NodeElement']);
|
||||
|
||||
foreach ($menus as $menuitem) {
|
||||
// Find the menu that was opened.
|
||||
$openmenu = $this->find('css', '.tox-selected-menu');
|
||||
|
||||
// Move the mouse to the first item in the list.
|
||||
// This is required because WebDriver takes the shortest path to the next click location,
|
||||
// which will mean crossing across other menu items.
|
||||
$firstlink = $openmenu->find('css', "[role^='menuitem'] .tox-collection__item-icon");
|
||||
$firstlink->mouseover();
|
||||
|
||||
// Now match by title where the role matches any menuitem, or menuitemcheckbox, or menuitem*.
|
||||
$link = $openmenu->find('css', "[title='{$menuitem}'][role^='menuitem']");
|
||||
$this->execute('behat_general::i_click_on', [$link, 'NodeElement']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the element type/index for the specified TinyMCE editor.
|
||||
*
|
||||
* @When /^I select the "(?P<textlocator_string>(?:[^"]|\\")*)" element in position "(?P<position_int>(?:[^"]|\\")*)" of the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor$/
|
||||
* @param string $textlocator The type of element to select (for example `p` or `span`)
|
||||
* @param int $position The zero-indexed position
|
||||
* @param string $locator The editor to select within
|
||||
*/
|
||||
public function select_text(string $textlocator, int $position, string $locator): void {
|
||||
$this->require_tiny_tags();
|
||||
|
||||
$editor = $this->get_textarea_for_locator($locator);
|
||||
$editorid = $editor->getAttribute('id');
|
||||
|
||||
// Ensure that a name is set on the iframe relating to the editorid.
|
||||
$js = <<<EOF
|
||||
const element = instance.dom.select("${textlocator}")[${position}];
|
||||
instance.selection.select(element);
|
||||
EOF;
|
||||
|
||||
$this->execute_javascript_for_editor($editorid, $js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file in the file picker using the repository_upload plugin.
|
||||
*
|
||||
* Note: This step assumes we are already in the file picker.
|
||||
* Note: This step is for use by TinyMCE and will be removed once an appropriate step is added to core.
|
||||
* See MDL-76001 for details.
|
||||
*
|
||||
* @Given /^I upload "(?P<filepath_string>(?:[^"]|\\")*)" to the file picker for TinyMCE$/
|
||||
*/
|
||||
public function i_upload_a_file_in_the_filepicker(string $filepath): void {
|
||||
if (!$this->has_tag('javascript')) {
|
||||
throw new DriverException('The file picker is only available with javascript enabled');
|
||||
}
|
||||
|
||||
if (!$this->has_tag('_file_upload')) {
|
||||
throw new DriverException('File upload tests must have the @_file_upload tag on either the scenario or feature.');
|
||||
}
|
||||
|
||||
if (!$this->has_tag('editor_tiny')) {
|
||||
throw new DriverException('This step is intended for use in TinyMCE. Please vote for MDL-76001');
|
||||
}
|
||||
|
||||
$filepicker = $this->find('dialogue', get_string('filepicker', 'core_repository'));
|
||||
|
||||
$this->execute('behat_general::i_click_on_in_the', [
|
||||
get_string('pluginname', 'repository_upload'), 'link',
|
||||
$filepicker, 'NodeElement',
|
||||
]);
|
||||
|
||||
$reporegion = $filepicker->find('css', '.fp-repo-items');
|
||||
$fileinput = $this->find('field', get_string('attachment', 'core_repository'), false, $reporegion);
|
||||
|
||||
$filepath = $this->normalise_fixture_filepath($filepath);
|
||||
|
||||
$fileinput->attachFile($filepath);
|
||||
$this->execute('behat_general::i_click_on_in_the', [
|
||||
get_string('upload', 'repository'), 'button',
|
||||
$reporegion, 'NodeElement',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the fixture file path relative to the dirroot.
|
||||
*
|
||||
* @param string $filepath
|
||||
* @return string
|
||||
*/
|
||||
protected function normalise_fixture_filepath(string $filepath): string {
|
||||
global $CFG;
|
||||
|
||||
$filepath = str_replace('/', DIRECTORY_SEPARATOR, $filepath);
|
||||
if (!is_readable($filepath)) {
|
||||
$filepath = $CFG->dirroot . DIRECTORY_SEPARATOR . $filepath;
|
||||
if (!is_readable($filepath)) {
|
||||
throw new ExpectationException('The file to be uploaded does not exist.', $this->getSession());
|
||||
}
|
||||
}
|
||||
|
||||
return $filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select in the editor.
|
||||
*
|
||||
* @param string $locator
|
||||
* @param string $type
|
||||
* @param string $editorlocator
|
||||
*
|
||||
* @Given /^I select the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)" in the "(?P<editorlocator_string>(?:[^"]|\\")*)" TinyMCE editor$/
|
||||
*/
|
||||
public function select_in_editor(string $locator, string $type, string $editorlocator): void {
|
||||
$this->require_tiny_tags();
|
||||
|
||||
$editor = $this->get_textarea_for_locator($editorlocator);
|
||||
$editorid = $editor->getAttribute('id');
|
||||
|
||||
// Get the iframe name for this editor.
|
||||
$iframename = $this->get_editor_iframe_name($editorlocator);
|
||||
|
||||
// Switch to it.
|
||||
$this->execute('behat_general::switch_to_iframe', [$iframename]);
|
||||
|
||||
// Find the element.
|
||||
$element = $this->find($type, $locator);
|
||||
$xpath = $element->getXpath();
|
||||
|
||||
// Switch back to the main window.
|
||||
$this->execute('behat_general::switch_to_the_main_frame', []);
|
||||
|
||||
// Select the Node using the xpath.
|
||||
$js = <<<EOF
|
||||
const editorDocument = instance.getDoc();
|
||||
const element = editorDocument.evaluate(
|
||||
"${xpath}",
|
||||
editorDocument,
|
||||
null,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||
null
|
||||
).singleNodeValue;
|
||||
|
||||
instance.selection.select(element);
|
||||
EOF;
|
||||
$this->execute_javascript_for_editor($editorid, $js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all of the TinyMCE toolbars.
|
||||
*
|
||||
* @Given /^I expand all toolbars for the "(?P<editorlocator_string>(?:[^"]|\\")*)" TinyMCE editor$/
|
||||
*
|
||||
* @param string $locator
|
||||
*/
|
||||
public function expand_all_toolbars(string $editorlocator): void {
|
||||
$this->require_tiny_tags();
|
||||
|
||||
$editor = $this->get_editor_container_for_locator($editorlocator);
|
||||
try {
|
||||
$button = $this->find('button', get_string('tiny:more...', 'editor_tiny'), false, $editor);
|
||||
} catch (ExpectationException $e) {
|
||||
// No more button, so no need to expand.
|
||||
return;
|
||||
}
|
||||
|
||||
if ($button->getAttribute(('aria-pressed')) === 'false') {
|
||||
$this->execute('behat_general::i_click_on', [$button, 'NodeElement']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
Reference in New Issue
Block a user