2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Steps definitions related with forms.
22 * @copyright 2012 David Monllaó
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
28 require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
29 require_once(__DIR__ . '/../../../lib/behat/behat_field_manager.php');
31 use Behat\Gherkin\Node\{TableNode, PyStringNode};
32 use Behat\Mink\Element\NodeElement;
33 use Behat\Mink\Exception\{ElementNotFoundException, ExpectationException};
36 * Forms-related steps definitions.
38 * Note, Behat tests to verify that the steps defined here work as advertised
39 * are kept in admin/tool/behat/tests/behat.
43 * @copyright 2012 David Monllaó
44 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46 class behat_forms extends behat_base {
49 * Presses button with specified id|name|title|alt|value.
51 * @When /^I press "(?P<button_string>(?:[^"]|\\")*)"$/
52 * @throws ElementNotFoundException Thrown by behat_base::find
53 * @param string $button
55 public function press_button($button) {
56 $this->execute('behat_general::i_click_on', [$button, 'button']);
60 * Press button with specified id|name|title|alt|value and switch to main window.
62 * @When /^I press "(?P<button_string>(?:[^"]|\\")*)" and switch to main window$/
63 * @throws ElementNotFoundException Thrown by behat_base::find
64 * @param string $button
66 public function press_button_and_switch_to_main_window($button) {
67 // Ensures the button is present, before pressing.
68 $buttonnode = $this->find_button($button);
71 // Switch to main window.
72 $this->getSession()->switchToWindow(behat_general::MAIN_WINDOW_NAME);
76 * Fills a form with field/value data.
78 * @Given /^I set the following fields to these values:$/
79 * @throws ElementNotFoundException Thrown by behat_base::find
80 * @param TableNode $data
82 public function i_set_the_following_fields_to_these_values(TableNode $data) {
84 // Expand all fields in case we have.
85 $this->expand_all_fields();
87 $datahash = $data->getRowsHash();
89 // The action depends on the field type.
90 foreach ($datahash as $locator => $value) {
91 $this->set_field_value($locator, $value);
96 * Fills a form with field/value data.
98 * @Given /^I set the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" to these values:$/
99 * @throws ElementNotFoundException Thrown by behat_base::find
100 * @param string $containerelement Element we look in
101 * @param string $containerselectortype The type of selector where we look in
102 * @param TableNode $data
104 public function i_set_the_following_fields_in_container_to_these_values(
105 $containerelement, $containerselectortype, TableNode $data) {
107 // Expand all fields in case we have.
108 $this->expand_all_fields();
110 $datahash = $data->getRowsHash();
112 // The action depends on the field type.
113 foreach ($datahash as $locator => $value) {
114 $this->set_field_value_in_container($locator, $value, $containerselectortype, $containerelement);
119 * Expands all moodleform's fields, including collapsed fieldsets and advanced fields if they are present.
120 * @Given /^I expand all fieldsets$/
122 public function i_expand_all_fieldsets() {
123 $this->expand_all_fields();
127 * Expands all moodle form fieldsets if they exists.
129 * Externalized from i_expand_all_fields to call it from
130 * other form-related steps without having to use steps-group calls.
132 * @throws ElementNotFoundException Thrown by behat_base::find_all
135 protected function expand_all_fields() {
136 // Expand only if JS mode, else not needed.
137 if (!$this->running_javascript()) {
141 // We already know that we waited for the DOM and the JS to be loaded, even the editor
142 // so, we will use the reduced timeout as it is a common task and we should save time.
145 // Expand all fieldsets link - which will only be there if there is more than one collapsible section.
146 $expandallxpath = "//div[@class='collapsible-actions']" .
147 "//a[contains(concat(' ', @class, ' '), ' collapseexpand ')]" .
148 "[not(contains(concat(' ', @class, ' '), ' collapse-all '))]";
149 // Else, look for the first expand fieldset link.
150 $expandonlysection = "//legend[@class='ftoggler']" .
151 "//a[contains(concat(' ', @class, ' '), ' fheader ') and @aria-expanded = 'false']";
153 $collapseexpandlink = $this->find('xpath', $expandallxpath . '|' . $expandonlysection,
154 false, false, behat_base::get_reduced_timeout());
155 $collapseexpandlink->click();
157 } catch (ElementNotFoundException $e) {
158 // The behat_base::find() method throws an exception if there are no elements,
159 // we should not fail a test because of this. We continue if there are not expandable fields.
162 // Different try & catch as we can have expanded fieldsets with advanced fields on them.
165 // Expand all fields xpath.
166 $showmorexpath = "//a[normalize-space(.)='" . get_string('showmore', 'form') . "']" .
167 "[contains(concat(' ', normalize-space(@class), ' '), ' moreless-toggler')]";
169 // We don't wait here as we already waited when getting the expand fieldsets links.
170 if (!$showmores = $this->getSession()->getPage()->findAll('xpath', $showmorexpath)) {
174 if ($this->getSession()->getDriver() instanceof \DMore\ChromeDriver\ChromeDriver) {
175 // Chrome Driver produces unique xpaths for each element.
176 foreach ($showmores as $showmore) {
180 // Funny thing about this, with findAll() we specify a pattern and each element matching the pattern
181 // is added to the array with of xpaths with a [0], [1]... sufix, but when we click on an element it
182 // does not matches the specified xpath anymore (now is a "Show less..." link) so [1] becomes [0],
183 // that's why we always click on the first XPath match, will be always the next one.
184 $iterations = count($showmores);
185 for ($i = 0; $i < $iterations; $i++) {
186 $showmores[0]->click();
190 } catch (ElementNotFoundException $e) {
191 // We continue with the test.
197 * Sets the field to wwwroot plus the given path. Include the first slash.
199 * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to local url "(?P<field_path_string>(?:[^"]|\\")*)"$/
200 * @throws ElementNotFoundException Thrown by behat_base::find
201 * @param string $field
202 * @param string $path
205 public function i_set_the_field_to_local_url($field, $path) {
207 $this->set_field_value($field, $CFG->wwwroot . $path);
211 * Sets the specified value to the field.
213 * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to "(?P<field_value_string>(?:[^"]|\\")*)"$/
214 * @throws ElementNotFoundException Thrown by behat_base::find
215 * @param string $field
216 * @param string $value
219 public function i_set_the_field_to($field, $value) {
220 $this->set_field_value($field, $value);
224 * Sets the specified value to the field.
226 * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" to "(?P<field_value_string>(?:[^"]|\\")*)"$/
227 * @throws ElementNotFoundException Thrown by behat_base::find
228 * @param string $field
229 * @param string $containerelement Element we look in
230 * @param string $containerselectortype The type of selector where we look in
231 * @param string $value
233 public function i_set_the_field_in_container_to($field, $containerelement, $containerselectortype, $value) {
234 $this->set_field_value_in_container($field, $value, $containerselectortype, $containerelement);
238 * Press the key in the field to trigger the javascript keypress event
240 * Note that the character key will not actually be typed in the input field
242 * @Given /^I press key "(?P<key_string>(?:[^"]|\\")*)" in the field "(?P<field_string>(?:[^"]|\\")*)"$/
243 * @throws ElementNotFoundException Thrown by behat_base::find
244 * @param string $key either char-code or character itself,
245 * may optionally be prefixed with ctrl-, alt-, shift- or meta-
246 * @param string $field
249 public function i_press_key_in_the_field($key, $field) {
250 if (!$this->running_javascript()) {
251 throw new DriverException('Key press step is not available with Javascript disabled');
253 $fld = behat_field_manager::get_form_field_from_label($field, $this);
256 if (preg_match('/-/', $key)) {
257 list($modifier, $char) = preg_split('/-/', $key, 2);
259 if (is_numeric($char)) {
262 $fld->key_press($char, $modifier);
266 * Sets the specified value to the field.
268 * @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to multiline:$/
269 * @throws ElementNotFoundException Thrown by behat_base::find
270 * @param string $field
271 * @param PyStringNode $value
274 public function i_set_the_field_to_multiline($field, PyStringNode $value) {
275 $this->set_field_value($field, (string)$value);
279 * Sets the specified value to the field with xpath.
281 * @Given /^I set the field with xpath "(?P<fieldxpath_string>(?:[^"]|\\")*)" to "(?P<field_value_string>(?:[^"]|\\")*)"$/
282 * @throws ElementNotFoundException Thrown by behat_base::find
283 * @param string $field
284 * @param string $value
287 public function i_set_the_field_with_xpath_to($fieldxpath, $value) {
288 $this->set_field_node_value($this->find('xpath', $fieldxpath), $value);
292 * Checks, the field matches the value.
294 * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" matches value "(?P<field_value_string>(?:[^"]|\\")*)"$/
295 * @throws ElementNotFoundException Thrown by behat_base::find
296 * @param string $field
297 * @param string $value
300 public function the_field_matches_value($field, $value) {
303 $formfield = behat_field_manager::get_form_field_from_label($field, $this);
305 // Checks if the provided value matches the current field value.
306 if (!$formfield->matches($value)) {
307 $fieldvalue = $formfield->get_value();
308 throw new ExpectationException(
309 'The \'' . $field . '\' value is \'' . $fieldvalue . '\', \'' . $value . '\' expected' ,
316 * Checks, the field does not match the value.
318 * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" does not match value "(?P<field_value_string>(?:[^"]|\\")*)"$/
319 * @throws ExpectationException
320 * @throws ElementNotFoundException Thrown by behat_base::find
321 * @param string $field
322 * @param string $value
324 public function the_field_does_not_match_value($field, $value) {
327 $formfield = behat_field_manager::get_form_field_from_label($field, $this);
329 // Checks if the provided value matches the current field value.
330 if ($formfield->matches($value)) {
331 throw new ExpectationException(
332 'The \'' . $field . '\' value matches \'' . $value . '\' and it should not match it' ,
339 * Checks, the field matches the value.
341 * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" matches value "(?P<field_value_string>(?:[^"]|\\")*)"$/
342 * @throws ElementNotFoundException Thrown by behat_base::find
343 * @param string $field
344 * @param string $containerelement Element we look in
345 * @param string $containerselectortype The type of selector where we look in
346 * @param string $value
348 public function the_field_in_container_matches_value($field, $containerelement, $containerselectortype, $value) {
351 $node = $this->get_node_in_container('field', $field, $containerselectortype, $containerelement);
352 $formfield = behat_field_manager::get_form_field($node, $this->getSession());
354 // Checks if the provided value matches the current field value.
355 if (!$formfield->matches($value)) {
356 $fieldvalue = $formfield->get_value();
357 throw new ExpectationException(
358 'The \'' . $field . '\' value is \'' . $fieldvalue . '\', \'' . $value . '\' expected' ,
365 * Checks, the field does not match the value.
367 * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" does not match value "(?P<field_value_string>(?:[^"]|\\")*)"$/
368 * @throws ExpectationException
369 * @throws ElementNotFoundException Thrown by behat_base::find
370 * @param string $field
371 * @param string $containerelement Element we look in
372 * @param string $containerselectortype The type of selector where we look in
373 * @param string $value
375 public function the_field_in_container_does_not_match_value($field, $containerelement, $containerselectortype, $value) {
378 $node = $this->get_node_in_container('field', $field, $containerselectortype, $containerelement);
379 $formfield = behat_field_manager::get_form_field($node, $this->getSession());
381 // Checks if the provided value matches the current field value.
382 if ($formfield->matches($value)) {
383 throw new ExpectationException(
384 'The \'' . $field . '\' value matches \'' . $value . '\' and it should not match it' ,
391 * Checks, the field matches the value.
393 * @Then /^the field with xpath "(?P<xpath_string>(?:[^"]|\\")*)" matches value "(?P<field_value_string>(?:[^"]|\\")*)"$/
394 * @throws ExpectationException
395 * @throws ElementNotFoundException Thrown by behat_base::find
396 * @param string $fieldxpath
397 * @param string $value
400 public function the_field_with_xpath_matches_value($fieldxpath, $value) {
403 $fieldnode = $this->find('xpath', $fieldxpath);
404 $formfield = behat_field_manager::get_form_field($fieldnode, $this->getSession());
406 // Checks if the provided value matches the current field value.
407 if (!$formfield->matches($value)) {
408 $fieldvalue = $formfield->get_value();
409 throw new ExpectationException(
410 'The \'' . $fieldxpath . '\' value is \'' . $fieldvalue . '\', \'' . $value . '\' expected' ,
417 * Checks, the field does not match the value.
419 * @Then /^the field with xpath "(?P<xpath_string>(?:[^"]|\\")*)" does not match value "(?P<field_value_string>(?:[^"]|\\")*)"$/
420 * @throws ExpectationException
421 * @throws ElementNotFoundException Thrown by behat_base::find
422 * @param string $fieldxpath
423 * @param string $value
426 public function the_field_with_xpath_does_not_match_value($fieldxpath, $value) {
429 $fieldnode = $this->find('xpath', $fieldxpath);
430 $formfield = behat_field_manager::get_form_field($fieldnode, $this->getSession());
432 // Checks if the provided value matches the current field value.
433 if ($formfield->matches($value)) {
434 throw new ExpectationException(
435 'The \'' . $fieldxpath . '\' value matches \'' . $value . '\' and it should not match it' ,
442 * Checks, the provided field/value matches.
444 * @Then /^the following fields match these values:$/
445 * @throws ExpectationException
446 * @param TableNode $data Pairs of | field | value |
448 public function the_following_fields_match_these_values(TableNode $data) {
450 // Expand all fields in case we have.
451 $this->expand_all_fields();
453 $datahash = $data->getRowsHash();
455 // The action depends on the field type.
456 foreach ($datahash as $locator => $value) {
457 $this->the_field_matches_value($locator, $value);
462 * Checks that the provided field/value pairs don't match.
464 * @Then /^the following fields do not match these values:$/
465 * @throws ExpectationException
466 * @param TableNode $data Pairs of | field | value |
468 public function the_following_fields_do_not_match_these_values(TableNode $data) {
470 // Expand all fields in case we have.
471 $this->expand_all_fields();
473 $datahash = $data->getRowsHash();
475 // The action depends on the field type.
476 foreach ($datahash as $locator => $value) {
477 $this->the_field_does_not_match_value($locator, $value);
482 * Checks, the provided field/value matches.
484 * @Then /^the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" match these values:$/
485 * @throws ExpectationException
486 * @param string $containerelement Element we look in
487 * @param string $containerselectortype The type of selector where we look in
488 * @param TableNode $data Pairs of | field | value |
490 public function the_following_fields_in_container_match_these_values(
491 $containerelement, $containerselectortype, TableNode $data) {
493 // Expand all fields in case we have.
494 $this->expand_all_fields();
496 $datahash = $data->getRowsHash();
498 // The action depends on the field type.
499 foreach ($datahash as $locator => $value) {
500 $this->the_field_in_container_matches_value($locator, $containerelement, $containerselectortype, $value);
505 * Checks that the provided field/value pairs don't match.
507 * @Then /^the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" do not match these values:$/
508 * @throws ExpectationException
509 * @param string $containerelement Element we look in
510 * @param string $containerselectortype The type of selector where we look in
511 * @param TableNode $data Pairs of | field | value |
513 public function the_following_fields_in_container_do_not_match_these_values(
514 $containerelement, $containerselectortype, TableNode $data) {
516 // Expand all fields in case we have.
517 $this->expand_all_fields();
519 $datahash = $data->getRowsHash();
521 // The action depends on the field type.
522 foreach ($datahash as $locator => $value) {
523 $this->the_field_in_container_does_not_match_value($locator, $containerelement, $containerselectortype, $value);
528 * Checks, that given select box contains the specified option.
530 * @Then /^the "(?P<select_string>(?:[^"]|\\")*)" select box should contain "(?P<option_string>(?:[^"]|\\")*)"$/
531 * @throws ExpectationException
532 * @throws ElementNotFoundException Thrown by behat_base::find
533 * @param string $select The select element name
534 * @param string $option The option text/value. Plain value or comma separated
535 * values if multiple. Commas in multiple values escaped with backslash.
537 public function the_select_box_should_contain($select, $option) {
539 $selectnode = $this->find_field($select);
540 $multiple = $selectnode->hasAttribute('multiple');
541 $optionsarr = array(); // Array of passed value/text options to test.
544 // Can pass multiple comma separated, with valuable commas escaped with backslash.
545 foreach (preg_replace('/\\\,/', ',', preg_split('/(?<!\\\),/', $option)) as $opt) {
546 $optionsarr[] = trim($opt);
549 // Only one option has been passed.
550 $optionsarr[] = trim($option);
553 // Now get all the values and texts in the select.
554 $options = $selectnode->findAll('xpath', '//option');
556 foreach ($options as $opt) {
557 $values[trim($opt->getValue())] = trim($opt->getText());
560 foreach ($optionsarr as $opt) {
561 // Verify every option is a valid text or value.
562 if (!in_array($opt, $values) && !array_key_exists($opt, $values)) {
563 throw new ExpectationException(
564 'The select box "' . $select . '" does not contain the option "' . $opt . '"',
572 * Checks, that given select box does not contain the specified option.
574 * @Then /^the "(?P<select_string>(?:[^"]|\\")*)" select box should not contain "(?P<option_string>(?:[^"]|\\")*)"$/
575 * @throws ExpectationException
576 * @throws ElementNotFoundException Thrown by behat_base::find
577 * @param string $select The select element name
578 * @param string $option The option text/value. Plain value or comma separated
579 * values if multiple. Commas in multiple values escaped with backslash.
581 public function the_select_box_should_not_contain($select, $option) {
583 $selectnode = $this->find_field($select);
584 $multiple = $selectnode->hasAttribute('multiple');
585 $optionsarr = array(); // Array of passed value/text options to test.
588 // Can pass multiple comma separated, with valuable commas escaped with backslash.
589 foreach (preg_replace('/\\\,/', ',', preg_split('/(?<!\\\),/', $option)) as $opt) {
590 $optionsarr[] = trim($opt);
593 // Only one option has been passed.
594 $optionsarr[] = trim($option);
597 // Now get all the values and texts in the select.
598 $options = $selectnode->findAll('xpath', '//option');
600 foreach ($options as $opt) {
601 $values[trim($opt->getValue())] = trim($opt->getText());
604 foreach ($optionsarr as $opt) {
605 // Verify every option is not a valid text or value.
606 if (in_array($opt, $values) || array_key_exists($opt, $values)) {
607 throw new ExpectationException(
608 'The select box "' . $select . '" contains the option "' . $opt . '"',
616 * Generic field setter.
618 * Internal API method, a generic *I set "VALUE" to "FIELD" field*
619 * could be created based on it.
621 * @param string $fieldlocator The pointer to the field, it will depend on the field type.
622 * @param string $value
625 protected function set_field_value($fieldlocator, $value) {
626 // We delegate to behat_form_field class, it will
627 // guess the type properly as it is a select tag.
628 $field = behat_field_manager::get_form_field_from_label($fieldlocator, $this);
629 $field->set_value($value);
633 * Generic field setter to be used by chainable steps.
635 * @param NodeElement $fieldnode
636 * @param string $value
638 public function set_field_node_value(NodeElement $fieldnode, string $value): void {
639 $this->ensure_node_is_visible($fieldnode);
640 $field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
641 $field->set_value($value);
645 * Generic field setter.
647 * Internal API method, a generic *I set "VALUE" to "FIELD" field*
648 * could be created based on it.
650 * @param string $fieldlocator The pointer to the field, it will depend on the field type.
651 * @param string $value the value to set
652 * @param string $containerselectortype The type of selector where we look in
653 * @param string $containerelement Element we look in
655 protected function set_field_value_in_container($fieldlocator, $value, $containerselectortype, $containerelement) {
656 $node = $this->get_node_in_container('field', $fieldlocator, $containerselectortype, $containerelement);
657 $this->set_field_node_value($node, $value);
661 * Select a value from single select and redirect.
663 * @Given /^I select "(?P<singleselect_option_string>(?:[^"]|\\")*)" from the "(?P<singleselect_name_string>(?:[^"]|\\")*)" singleselect$/
665 public function i_select_from_the_singleselect($option, $singleselect) {
667 $this->execute('behat_forms::i_set_the_field_to', array($this->escape($singleselect), $this->escape($option)));
669 if (!$this->running_javascript()) {
670 // Press button in the specified select container.
671 $containerxpath = "//div[" .
672 "(contains(concat(' ', normalize-space(@class), ' '), ' singleselect ') " .
673 "or contains(concat(' ', normalize-space(@class), ' '), ' urlselect ')".
675 .//label[contains(normalize-space(string(.)), '" . $singleselect . "')] " .
676 "or .//select[(./@name='" . $singleselect . "' or ./@id='". $singleselect . "')]" .
679 $this->execute('behat_general::i_click_on_in_the',
680 array(get_string('go'), "button", $containerxpath, "xpath_element")
686 * Select item from autocomplete list.
688 * @Given /^I click on "([^"]*)" item in the autocomplete list$/
690 * @param string $item
692 public function i_click_on_item_in_the_autocomplete_list($item) {
693 $xpathtarget = "//ul[@class='form-autocomplete-suggestions']//*[contains(concat('|', string(.), '|'),'|" . $item . "|')]";
695 $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
697 $this->execute('behat_general::i_press_key_in_element', ['13', 'body', 'xpath_element']);
701 * Open the auto-complete suggestions list (Assuming there is only one on the page.).
703 * @Given /^I open the autocomplete suggestions list$/
705 public function i_open_the_autocomplete_suggestions_list() {
706 $csstarget = ".form-autocomplete-downarrow";
707 $this->execute('behat_general::i_click_on', [$csstarget, 'css_element']);
711 * Expand the given autocomplete list
713 * @Given /^I expand the "(?P<field_string>(?:[^"]|\\")*)" autocomplete$/
715 * @param string $field Field name
717 public function i_expand_the_autocomplete($field) {
718 $csstarget = '.form-autocomplete-downarrow';
719 $node = $this->get_node_in_container('css_element', $csstarget, 'form_row', $field);
720 $this->ensure_node_is_visible($node);
725 * Assert the given option exist in the given autocomplete list
727 * @Given /^I should see "(?P<option_string>(?:[^"]|\\")*)" in the list of options for the "(?P<field_string>(?:[^"]|\\")*)" autocomplete$$/
729 * @param string $option Name of option
730 * @param string $field Field name
732 public function i_should_see_in_the_list_of_option_for_the_autocomplete($option, $field) {
733 $xpathtarget = "//div[contains(@class, 'form-autocomplete-selection') and contains(.//div, '" . $option . "')]";
734 $node = $this->get_node_in_container('xpath_element', $xpathtarget, 'form_row', $field);
735 $this->ensure_node_is_visible($node);