| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 3 | //
 | 
        
           |  |  | 4 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 5 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 6 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 7 | // (at your option) any later version.
 | 
        
           |  |  | 8 | //
 | 
        
           |  |  | 9 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 12 | // GNU General Public License for more details.
 | 
        
           |  |  | 13 | //
 | 
        
           |  |  | 14 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 15 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | /**
 | 
        
           |  |  | 18 |  * TinyMCE custom steps definitions.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package    editor_tiny
 | 
        
           |  |  | 21 |  * @category   test
 | 
        
           |  |  | 22 |  * @copyright  2022 Andrew Lyons <andrew@nicols.co.uk>
 | 
        
           |  |  | 23 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 24 |  */
 | 
        
           |  |  | 25 |   | 
        
           |  |  | 26 | use Behat\Behat\Hook\Scope\BeforeScenarioScope;
 | 
        
           |  |  | 27 | use Behat\Mink\Exception\DriverException;
 | 
        
           |  |  | 28 | use Behat\Mink\Exception\ExpectationException;
 | 
        
           |  |  | 29 |   | 
        
           |  |  | 30 | // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 | 
        
           |  |  | 31 | require_once(__DIR__ . '/../../../../behat/behat_base.php');
 | 
        
           |  |  | 32 | require_once(__DIR__ . '/editor_tiny_helpers.php');
 | 
        
           |  |  | 33 |   | 
        
           |  |  | 34 | /**
 | 
        
           |  |  | 35 |  * TinyMCE custom behat step definitions.
 | 
        
           |  |  | 36 |  *
 | 
        
           |  |  | 37 |  * @package    editor_tiny
 | 
        
           |  |  | 38 |  * @category   test
 | 
        
           |  |  | 39 |  * @copyright  2022 Andrew Lyons <andrew@nicols.co.uk>
 | 
        
           |  |  | 40 |  */
 | 
        
           |  |  | 41 | class behat_editor_tiny extends behat_base implements \core_behat\settable_editor {
 | 
        
           |  |  | 42 |     use editor_tiny_helpers;
 | 
        
           |  |  | 43 |   | 
        
           |  |  | 44 |     /**
 | 
        
           |  |  | 45 |      * Set Tiny as default editor before executing Tiny tests.
 | 
        
           |  |  | 46 |      *
 | 
        
           |  |  | 47 |      * This step is required to ensure that TinyMCE is set as the current default editor as it may
 | 
        
           |  |  | 48 |      * not always be the default editor.
 | 
        
           |  |  | 49 |      *
 | 
        
           |  |  | 50 |      * Any Scenario, or Feature, which has the `editor_tiny` tag, or any `tiny_*` tag will have
 | 
        
           |  |  | 51 |      * this step executed before the Scenario.
 | 
        
           |  |  | 52 |      *
 | 
        
           |  |  | 53 |      * @BeforeScenario
 | 
        
           |  |  | 54 |      * @param BeforeScenarioScope $scope The Behat Scope
 | 
        
           |  |  | 55 |      */
 | 
        
           |  |  | 56 |     public function set_default_editor_flag(BeforeScenarioScope $scope): void {
 | 
        
           |  |  | 57 |         // This only applies to a scenario which matches the editor_tiny, or an tiny subplugin.
 | 
        
           |  |  | 58 |         $callback = function (string $tag): bool {
 | 
        
           |  |  | 59 |             return $tag === 'editor_tiny' || substr($tag, 0, 5) === 'tiny_';
 | 
        
           |  |  | 60 |         };
 | 
        
           |  |  | 61 |   | 
        
           |  |  | 62 |         if (!self::scope_tags_match($scope, $callback)) {
 | 
        
           |  |  | 63 |             // This scope does not require TinyMCE. Exit now.
 | 
        
           |  |  | 64 |             return;
 | 
        
           |  |  | 65 |         }
 | 
        
           |  |  | 66 |   | 
        
           |  |  | 67 |         // TinyMCE is a JavaScript editor so require JS here.
 | 
        
           |  |  | 68 |         $this->require_javascript();
 | 
        
           |  |  | 69 |   | 
        
           |  |  | 70 |         $this->execute('behat_general::the_default_editor_is_set_to', ['tiny']);
 | 
        
           |  |  | 71 |     }
 | 
        
           |  |  | 72 |   | 
        
           |  |  | 73 |     /**
 | 
        
           |  |  | 74 |      * Click on a button for the specified TinyMCE editor.
 | 
        
           |  |  | 75 |      *
 | 
        
           |  |  | 76 |      * @When /^I click on the "(?P<button_string>(?:[^"]|\\")*)" button for the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor$/
 | 
        
           |  |  | 77 |      *
 | 
        
           |  |  | 78 |      * @param string $button The label of the button
 | 
        
           |  |  | 79 |      * @param string $locator The locator for the editor
 | 
        
           |  |  | 80 |      */
 | 
        
           |  |  | 81 |     public function i_click_on_button(string $button, string $locator): void {
 | 
        
           |  |  | 82 |         $this->require_tiny_tags();
 | 
        
           |  |  | 83 |         $container = $this->get_editor_container_for_locator($locator);
 | 
        
           |  |  | 84 |   | 
        
           |  |  | 85 |         $this->execute('behat_general::i_click_on_in_the', [$button, 'button', $container, 'NodeElement']);
 | 
        
           |  |  | 86 |     }
 | 
        
           |  |  | 87 |   | 
        
           |  |  | 88 |     /**
 | 
        
           | 1441 | ariadna | 89 |      * Checks that a button exists in specified TinyMCE editor.
 | 
        
           |  |  | 90 |      *
 | 
        
           |  |  | 91 |      * @When /^"(?P<button_string>(?:[^"]|\\")*)" button should exist in the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor$/
 | 
        
           |  |  | 92 |      *
 | 
        
           |  |  | 93 |      * @param string $button The label of the button
 | 
        
           |  |  | 94 |      * @param string $locator The locator for the editor
 | 
        
           |  |  | 95 |      */
 | 
        
           |  |  | 96 |     public function button_should_exist(string $button, string $locator): void {
 | 
        
           |  |  | 97 |         $this->require_tiny_tags();
 | 
        
           |  |  | 98 |         $container = $this->get_editor_container_for_locator($locator);
 | 
        
           |  |  | 99 |   | 
        
           |  |  | 100 |         $this->execute('behat_general::should_exist_in_the', [$button, 'button', $container, 'NodeElement']);
 | 
        
           |  |  | 101 |     }
 | 
        
           |  |  | 102 |   | 
        
           |  |  | 103 |     /**
 | 
        
           |  |  | 104 |      * Checks that a button does not exist in specified TinyMCE editor.
 | 
        
           |  |  | 105 |      *
 | 
        
           |  |  | 106 |      * @When /^"(?P<button_string>(?:[^"]|\\")*)" button should not exist in the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor$/
 | 
        
           |  |  | 107 |      *
 | 
        
           |  |  | 108 |      * @param string $button The label of the button
 | 
        
           |  |  | 109 |      * @param string $locator The locator for the editor
 | 
        
           |  |  | 110 |      */
 | 
        
           |  |  | 111 |     public function button_should_not_exist(string $button, string $locator): void {
 | 
        
           |  |  | 112 |         $this->require_tiny_tags();
 | 
        
           |  |  | 113 |         $container = $this->get_editor_container_for_locator($locator);
 | 
        
           |  |  | 114 |   | 
        
           |  |  | 115 |         $this->execute('behat_general::should_not_exist_in_the', [$button, 'button', $container, 'NodeElement']);
 | 
        
           |  |  | 116 |     }
 | 
        
           |  |  | 117 |   | 
        
           |  |  | 118 |     /**
 | 
        
           | 1 | efrain | 119 |      * Confirm that the button state of the specified button/editor combination matches the expectation.
 | 
        
           |  |  | 120 |      *
 | 
        
           |  |  | 121 |      * @Then /^the "(?P<button_string>(?:[^"]|\\")*)" button of the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor has state "(?P<state_string>(?:[^"]|\\")*)"$/
 | 
        
           |  |  | 122 |      *
 | 
        
           |  |  | 123 |      * @param string $button The text name of the button
 | 
        
           |  |  | 124 |      * @param string $locator The locator string for the editor
 | 
        
           |  |  | 125 |      * @param string $state The state of the button
 | 
        
           |  |  | 126 |      * @throws ExpectationException Thrown if the button state is not correct
 | 
        
           |  |  | 127 |      */
 | 
        
           |  |  | 128 |     public function button_state_is(string $button, string $locator, string $state): void {
 | 
        
           |  |  | 129 |         $this->require_tiny_tags();
 | 
        
           |  |  | 130 |         $container = $this->get_editor_container_for_locator($locator);
 | 
        
           |  |  | 131 |   | 
        
           |  |  | 132 |         $button = $this->find_button($button, false, $container);
 | 
        
           |  |  | 133 |         $buttonstate = $button->getAttribute('aria-pressed');
 | 
        
           |  |  | 134 |   | 
        
           |  |  | 135 |         if ($buttonstate !== $state) {
 | 
        
           |  |  | 136 |             throw new ExpectationException("Button '{$button}' is in state '{$buttonstate}' not '{$state}'", $this->getSession());
 | 
        
           |  |  | 137 |         }
 | 
        
           |  |  | 138 |     }
 | 
        
           |  |  | 139 |   | 
        
           |  |  | 140 |     /**
 | 
        
           |  |  | 141 |      * Click on a button for the specified TinyMCE editor.
 | 
        
           |  |  | 142 |      *
 | 
        
           |  |  | 143 |      * @When /^I click on the "(?P<menuitem_string>(?:[^"]|\\")*)" menu item for the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor$/
 | 
        
           |  |  | 144 |      *
 | 
        
           |  |  | 145 |      * @param string $menuitem The label of the menu item
 | 
        
           |  |  | 146 |      * @param string $locator The locator for the editor
 | 
        
           |  |  | 147 |      */
 | 
        
           |  |  | 148 |     public function i_click_on_menuitem_in_menu(string $menuitem, string $locator): void {
 | 
        
           |  |  | 149 |         $this->require_tiny_tags();
 | 
        
           |  |  | 150 |         $container = $this->get_editor_container_for_locator($locator);
 | 
        
           |  |  | 151 |   | 
        
           |  |  | 152 |         $menubar = $container->find('css', '[role="menubar"]');
 | 
        
           |  |  | 153 |   | 
        
           |  |  | 154 |         $menus = array_map(function(string $value): string {
 | 
        
           |  |  | 155 |             return trim($value);
 | 
        
           |  |  | 156 |         }, explode('>', $menuitem));
 | 
        
           |  |  | 157 |   | 
        
           |  |  | 158 |         // Open the menu bar.
 | 
        
           |  |  | 159 |         $mainmenu = array_shift($menus);
 | 
        
           |  |  | 160 |         $this->execute('behat_general::i_click_on_in_the', [$mainmenu, 'button', $menubar, 'NodeElement']);
 | 
        
           |  |  | 161 |   | 
        
           |  |  | 162 |         foreach ($menus as $menuitem) {
 | 
        
           |  |  | 163 |             // Find the menu that was opened.
 | 
        
           |  |  | 164 |             $openmenu = $this->find('css', '.tox-selected-menu');
 | 
        
           |  |  | 165 |   | 
        
           |  |  | 166 |             // Move the mouse to the first item in the list.
 | 
        
           |  |  | 167 |             // This is required because WebDriver takes the shortest path to the next click location,
 | 
        
           |  |  | 168 |             // which will mean crossing across other menu items.
 | 
        
           |  |  | 169 |             $firstlink = $openmenu->find('css', "[role^='menuitem'] .tox-collection__item-icon");
 | 
        
           |  |  | 170 |             $firstlink->mouseover();
 | 
        
           |  |  | 171 |   | 
        
           |  |  | 172 |             // Now match by title where the role matches any menuitem, or menuitemcheckbox, or menuitem*.
 | 
        
           | 1441 | ariadna | 173 |             $link = $openmenu->find('css', "[aria-label='{$menuitem}'][role^='menuitem']");
 | 
        
           | 1 | efrain | 174 |             $this->execute('behat_general::i_click_on', [$link, 'NodeElement']);
 | 
        
           |  |  | 175 |         }
 | 
        
           |  |  | 176 |     }
 | 
        
           |  |  | 177 |   | 
        
           |  |  | 178 |     /**
 | 
        
           |  |  | 179 |      * Select the element type/index for the specified TinyMCE editor.
 | 
        
           |  |  | 180 |      *
 | 
        
           |  |  | 181 |      * @When /^I select the "(?P<textlocator_string>(?:[^"]|\\")*)" element in position "(?P<position_int>(?:[^"]|\\")*)" of the "(?P<locator_string>(?:[^"]|\\")*)" TinyMCE editor$/
 | 
        
           |  |  | 182 |      * @param string $textlocator The type of element to select (for example `p` or `span`)
 | 
        
           |  |  | 183 |      * @param int $position The zero-indexed position
 | 
        
           |  |  | 184 |      * @param string $locator The editor to select within
 | 
        
           |  |  | 185 |      */
 | 
        
           |  |  | 186 |     public function select_text(string $textlocator, int $position, string $locator): void {
 | 
        
           |  |  | 187 |         $this->require_tiny_tags();
 | 
        
           |  |  | 188 |   | 
        
           |  |  | 189 |         $editor = $this->get_textarea_for_locator($locator);
 | 
        
           |  |  | 190 |         $editorid = $editor->getAttribute('id');
 | 
        
           |  |  | 191 |   | 
        
           |  |  | 192 |         // Ensure that a name is set on the iframe relating to the editorid.
 | 
        
           |  |  | 193 |         $js = <<<EOF
 | 
        
           |  |  | 194 |             const element = instance.dom.select("{$textlocator}")[{$position}];
 | 
        
           |  |  | 195 |             instance.selection.select(element);
 | 
        
           |  |  | 196 |         EOF;
 | 
        
           |  |  | 197 |   | 
        
           |  |  | 198 |         $this->execute_javascript_for_editor($editorid, $js);
 | 
        
           |  |  | 199 |     }
 | 
        
           |  |  | 200 |   | 
        
           |  |  | 201 |     /**
 | 
        
           |  |  | 202 |      * Upload a file in the file picker using the repository_upload plugin.
 | 
        
           |  |  | 203 |      *
 | 
        
           |  |  | 204 |      * Note: This step assumes we are already in the file picker.
 | 
        
           |  |  | 205 |      * Note: This step is for use by TinyMCE and will be removed once an appropriate step is added to core.
 | 
        
           |  |  | 206 |      * See MDL-76001 for details.
 | 
        
           |  |  | 207 |      *
 | 
        
           |  |  | 208 |      * @Given /^I upload "(?P<filepath_string>(?:[^"]|\\")*)" to the file picker for TinyMCE$/
 | 
        
           |  |  | 209 |      */
 | 
        
           |  |  | 210 |     public function i_upload_a_file_in_the_filepicker(string $filepath): void {
 | 
        
           |  |  | 211 |         if (!$this->has_tag('javascript')) {
 | 
        
           |  |  | 212 |             throw new DriverException('The file picker is only available with javascript enabled');
 | 
        
           |  |  | 213 |         }
 | 
        
           |  |  | 214 |   | 
        
           |  |  | 215 |         if (!$this->has_tag('_file_upload')) {
 | 
        
           |  |  | 216 |             throw new DriverException('File upload tests must have the @_file_upload tag on either the scenario or feature.');
 | 
        
           |  |  | 217 |         }
 | 
        
           |  |  | 218 |   | 
        
           |  |  | 219 |         if (!$this->has_tag('editor_tiny')) {
 | 
        
           |  |  | 220 |             throw new DriverException('This step is intended for use in TinyMCE. Please vote for MDL-76001');
 | 
        
           |  |  | 221 |         }
 | 
        
           |  |  | 222 |   | 
        
           |  |  | 223 |         $filepicker = $this->find('dialogue', get_string('filepicker', 'core_repository'));
 | 
        
           |  |  | 224 |   | 
        
           |  |  | 225 |         $this->execute('behat_general::i_click_on_in_the', [
 | 
        
           |  |  | 226 |             get_string('pluginname', 'repository_upload'), 'link',
 | 
        
           |  |  | 227 |             $filepicker, 'NodeElement',
 | 
        
           |  |  | 228 |         ]);
 | 
        
           |  |  | 229 |   | 
        
           |  |  | 230 |         $reporegion = $filepicker->find('css', '.fp-repo-items');
 | 
        
           |  |  | 231 |         $fileinput = $this->find('field', get_string('attachment', 'core_repository'), false, $reporegion);
 | 
        
           |  |  | 232 |   | 
        
           |  |  | 233 |         $filepath = $this->normalise_fixture_filepath($filepath);
 | 
        
           |  |  | 234 |   | 
        
           |  |  | 235 |         $fileinput->attachFile($filepath);
 | 
        
           |  |  | 236 |         $this->execute('behat_general::i_click_on_in_the', [
 | 
        
           |  |  | 237 |             get_string('upload', 'repository'), 'button',
 | 
        
           |  |  | 238 |             $reporegion, 'NodeElement',
 | 
        
           |  |  | 239 |         ]);
 | 
        
           |  |  | 240 |     }
 | 
        
           |  |  | 241 |   | 
        
           |  |  | 242 |     /**
 | 
        
           |  |  | 243 |      * Select in the editor.
 | 
        
           |  |  | 244 |      *
 | 
        
           |  |  | 245 |      * @param string $locator
 | 
        
           |  |  | 246 |      * @param string $type
 | 
        
           |  |  | 247 |      * @param string $editorlocator
 | 
        
           |  |  | 248 |      *
 | 
        
           |  |  | 249 |      * @Given /^I select the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)" in the "(?P<editorlocator_string>(?:[^"]|\\")*)" TinyMCE editor$/
 | 
        
           |  |  | 250 |      */
 | 
        
           |  |  | 251 |     public function select_in_editor(string $locator, string $type, string $editorlocator): void {
 | 
        
           |  |  | 252 |         $this->require_tiny_tags();
 | 
        
           |  |  | 253 |   | 
        
           |  |  | 254 |         $editor = $this->get_textarea_for_locator($editorlocator);
 | 
        
           |  |  | 255 |         $editorid = $editor->getAttribute('id');
 | 
        
           |  |  | 256 |   | 
        
           |  |  | 257 |         // Get the iframe name for this editor.
 | 
        
           |  |  | 258 |         $iframename = $this->get_editor_iframe_name($editorlocator);
 | 
        
           |  |  | 259 |   | 
        
           |  |  | 260 |         // Switch to it.
 | 
        
           |  |  | 261 |         $this->execute('behat_general::switch_to_iframe', [$iframename]);
 | 
        
           |  |  | 262 |   | 
        
           |  |  | 263 |         // Find the element.
 | 
        
           |  |  | 264 |         $element = $this->find($type, $locator);
 | 
        
           |  |  | 265 |         $xpath = $element->getXpath();
 | 
        
           |  |  | 266 |   | 
        
           |  |  | 267 |         // Switch back to the main window.
 | 
        
           |  |  | 268 |         $this->execute('behat_general::switch_to_the_main_frame', []);
 | 
        
           |  |  | 269 |   | 
        
           |  |  | 270 |         // Select the Node using the xpath.
 | 
        
           |  |  | 271 |         $js = <<<EOF
 | 
        
           |  |  | 272 |             const editorDocument = instance.getDoc();
 | 
        
           |  |  | 273 |             const element = editorDocument.evaluate(
 | 
        
           |  |  | 274 |                 "{$xpath}",
 | 
        
           |  |  | 275 |                 editorDocument,
 | 
        
           |  |  | 276 |                 null,
 | 
        
           |  |  | 277 |                 XPathResult.FIRST_ORDERED_NODE_TYPE,
 | 
        
           |  |  | 278 |                 null
 | 
        
           |  |  | 279 |             ).singleNodeValue;
 | 
        
           |  |  | 280 |   | 
        
           |  |  | 281 |             instance.selection.select(element);
 | 
        
           |  |  | 282 |         EOF;
 | 
        
           |  |  | 283 |         $this->execute_javascript_for_editor($editorid, $js);
 | 
        
           |  |  | 284 |     }
 | 
        
           |  |  | 285 |   | 
        
           |  |  | 286 |     /**
 | 
        
           |  |  | 287 |      * Expand all of the TinyMCE toolbars.
 | 
        
           |  |  | 288 |      *
 | 
        
           |  |  | 289 |      * @Given /^I expand all toolbars for the "(?P<editorlocator_string>(?:[^"]|\\")*)" TinyMCE editor$/
 | 
        
           |  |  | 290 |      *
 | 
        
           |  |  | 291 |      * @param string $locator
 | 
        
           |  |  | 292 |      */
 | 
        
           |  |  | 293 |     public function expand_all_toolbars(string $editorlocator): void {
 | 
        
           |  |  | 294 |         $this->require_tiny_tags();
 | 
        
           |  |  | 295 |   | 
        
           |  |  | 296 |         $editor = $this->get_editor_container_for_locator($editorlocator);
 | 
        
           |  |  | 297 |         try {
 | 
        
           |  |  | 298 |             $button = $this->find('button', get_string('tiny:reveal_or_hide_additional_toolbar_items', 'editor_tiny'), false, $editor);
 | 
        
           |  |  | 299 |         } catch (ExpectationException $e) {
 | 
        
           |  |  | 300 |             // No more button, so no need to expand.
 | 
        
           |  |  | 301 |             return;
 | 
        
           |  |  | 302 |         }
 | 
        
           |  |  | 303 |   | 
        
           | 1441 | ariadna | 304 |         if ($button->getAttribute(('aria-expanded')) === 'false') {
 | 
        
           | 1 | efrain | 305 |             $this->execute('behat_general::i_click_on', [$button, 'NodeElement']);
 | 
        
           |  |  | 306 |         }
 | 
        
           |  |  | 307 |     }
 | 
        
           |  |  | 308 |   | 
        
           |  |  | 309 |     /**
 | 
        
           |  |  | 310 |      * Switch to the TinyMCE iframe using a selector.
 | 
        
           |  |  | 311 |      *
 | 
        
           |  |  | 312 |      * @param string $editorlocator
 | 
        
           |  |  | 313 |      *
 | 
        
           |  |  | 314 |      * @When /^I switch to the "(?P<editorlocator_string>(?:[^"]|\\")*)" TinyMCE editor iframe$/
 | 
        
           |  |  | 315 |      */
 | 
        
           |  |  | 316 |     public function switch_to_tiny_iframe(string $editorlocator): void {
 | 
        
           |  |  | 317 |         $this->require_tiny_tags();
 | 
        
           |  |  | 318 |   | 
        
           |  |  | 319 |         // Get the iframe name for this editor.
 | 
        
           |  |  | 320 |         $iframename = $this->get_editor_iframe_name($editorlocator);
 | 
        
           |  |  | 321 |   | 
        
           |  |  | 322 |         // Switch to it.
 | 
        
           |  |  | 323 |         $this->execute('behat_general::switch_to_iframe', [$iframename]);
 | 
        
           |  |  | 324 |     }
 | 
        
           |  |  | 325 | }
 |