diff --git a/.upgradenotes/MDL-86231-2025080601561269.yml b/.upgradenotes/MDL-86231-2025080601561269.yml new file mode 100644 index 00000000000..88a84837b9a --- /dev/null +++ b/.upgradenotes/MDL-86231-2025080601561269.yml @@ -0,0 +1,24 @@ +issueNumber: MDL-86231 +notes: + core: + - message: > + The Behat `::execute()` method now accepts an array-style callable in + addition to the string `classname::method` format. + + + The following formats are now accepted: + + + ```php + + // String format: + + $this->execute('behat_general::i_click_on', [...]); + + + // Array format: + + $this->execute([behat_general::class,' i_click_on'], [...]); + + ``` + type: improved diff --git a/lib/behat/classes/behat_session_trait.php b/lib/behat/classes/behat_session_trait.php index cc7aa363300..bc4a810ee7b 100644 --- a/lib/behat/classes/behat_session_trait.php +++ b/lib/behat/classes/behat_session_trait.php @@ -1109,19 +1109,37 @@ EOF; /** * Helper function to execute api in a given context. * - * @param string $contextapi context in which api is defined. + * Note: The contextapi does not support a callback. + * + * @param string|array $contextapi context in which api is defined. * @param array|mixed $params list of params to pass or a single parameter * @throws Exception + * @throws DriverException */ - protected function execute($contextapi, $params = array()) { + protected function execute( + string|array $contextapi, + mixed $params = [], + ): void { if (!is_array($params)) { - $params = array($params); + $params = [$params]; + } + + if (is_string($contextapi)) { + $contextapi = explode('::', $contextapi); + } + + if (count($contextapi) !== 2) { + throw new DriverException('Invalid contextapi format, expected "context::api" or ["context", "api"]'); } // Get required context and execute the api. - $contextapi = explode("::", $contextapi); - $context = behat_context_helper::get($contextapi[0]); - call_user_func_array(array($context, $contextapi[1]), $params); + [$classname, $method] = $contextapi; + if (!is_string($classname) || !is_string($method)) { + throw new DriverException('Invalid contextapi format, expected "context::api" or ["context", "api"]'); + } + + $context = behat_context_helper::get($classname); + call_user_func_array([$context, $method], $params); // NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results. // Don't make them optional for performance reasons. diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index 303b01daa30..c220b018154 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -78,7 +78,7 @@ class behat_general extends behat_base { * @Given /^I am on homepage$/ */ public function i_am_on_homepage() { - $this->execute('behat_general::i_visit', ['/']); + $this->execute([self::class, 'i_visit'], ['/']); } /** @@ -87,7 +87,7 @@ class behat_general extends behat_base { * @Given /^I am on site homepage$/ */ public function i_am_on_site_homepage() { - $this->execute('behat_general::i_visit', ['/?redirect=0']); + $this->execute([self::class, 'i_visit'], ['/?redirect=0']); } /** @@ -96,7 +96,7 @@ class behat_general extends behat_base { * @Given /^I am on course index$/ */ public function i_am_on_course_index() { - $this->execute('behat_general::i_visit', ['/course/index.php']); + $this->execute([self::class, 'i_visit'], ['/course/index.php']); } /** @@ -462,7 +462,7 @@ class behat_general extends behat_base { */ public function i_click_on_confirming_the_dialogue($element, $selectortype) { $this->i_click_on($element, $selectortype); - $this->execute('behat_general::accept_currently_displayed_alert_dialog', []); + $this->execute([self::class, 'accept_currently_displayed_alert_dialog'], []); $this->wait_until_the_page_is_ready(); } @@ -476,7 +476,7 @@ class behat_general extends behat_base { */ public function i_click_on_dismissing_the_dialogue($element, $selectortype) { $this->i_click_on($element, $selectortype); - $this->execute('behat_general::dismiss_currently_displayed_alert_dialog', []); + $this->execute([self::class, 'dismiss_currently_displayed_alert_dialog'], []); $this->wait_until_the_page_is_ready(); } @@ -1165,7 +1165,7 @@ EOF; * @Given /^I trigger cron$/ */ public function i_trigger_cron() { - $this->execute('behat_general::i_visit', ['/admin/cron.php']); + $this->execute([self::class, 'i_visit'], ['/admin/cron.php']); } /** @@ -2151,8 +2151,8 @@ EOF; } // Gets the node based on the requested selector type and locator. $node = $this->get_selected_node($selectortype, $element); - $this->execute('behat_general::i_click_on', [$node, 'NodeElement']); - $this->execute('behat_general::i_press_named_key', ['', 'tab']); + $this->execute([self::class, 'i_click_on'], [$node, 'NodeElement']); + $this->execute([self::class, 'i_press_named_key'], ['', 'tab']); } /** @@ -2265,9 +2265,9 @@ EOF; */ public function i_manually_press_tab($shift = '') { if (empty($shift)) { - $this->execute('behat_general::i_press_named_key', ['', 'tab']); + $this->execute([self::class, 'i_press_named_key'], ['', 'tab']); } else { - $this->execute('behat_general::i_press_named_key', ['shift', 'tab']); + $this->execute([self::class, 'i_press_named_key'], ['shift', 'tab']); } } @@ -2363,7 +2363,7 @@ EOF; * @throws DriverException */ public function i_manually_press_enter() { - $this->execute('behat_general::i_press_named_key', ['', 'enter']); + $this->execute([self::class, 'i_press_named_key'], ['', 'enter']); } /** @@ -2398,7 +2398,7 @@ EOF; */ public function i_click_on_the_dynamic_tab(string $tabname): void { $xpath = "//*[@id='dynamictabs-tabs'][descendant::a[contains(text(), '" . $this->escape($tabname) . "')]]"; - $this->execute('behat_general::i_click_on_in_the', + $this->execute([self::class, 'i_click_on_in_the'], [$tabname, 'link', $xpath, 'xpath_element']); } @@ -2549,9 +2549,9 @@ EOF; // Update the state of the switch. $field = $node->getAttribute('id'); if ($state == "on") { - $this->execute('behat_forms::i_set_the_field_to', [$field, 1]); + $this->execute([behat_forms::class, 'i_set_the_field_to'], [$field, 1]); } else if ($state == "off") { - $this->execute('behat_forms::i_set_the_field_to', [$field, 0]); + $this->execute([behat_forms::class, 'i_set_the_field_to'], [$field, 0]); } else { throw new \Behat\Mink\Exception\ExpectationException('Invalid state for switch: ' . $state, $this->getSession()); } @@ -2585,7 +2585,7 @@ EOF; * @throws coding_exception */ private function get_combobox_dropdown_node(string $comboboxname, string $itemname, bool $fieldset = true): NodeElement { - $this->execute("behat_general::wait_until_the_page_is_ready"); + $this->execute([self::class, 'wait_until_the_page_is_ready']); $comboboxxpath = "//div[contains(@class, 'comboboxsearch') and .//span[text()='{$comboboxname}']]"; $dropdowntriggerxpath = $comboboxxpath . "/descendant::div[contains(@class,'dropdown-toggle')]"; @@ -2594,12 +2594,12 @@ EOF; // If the dropdown is not visible, open it. Also, ensure that a dropdown trigger element exists. if ($this->getSession()->getPage()->find('xpath', $dropdowntriggerxpath) && !$dropdown->isVisible()) { - $this->execute("behat_general::i_click_on", [$dropdowntriggerxpath, "xpath_element"]); + $this->execute([self::class, 'i_click_on'], [$dropdowntriggerxpath, "xpath_element"]); } if ($fieldset) { - $this->execute("behat_forms::set_field_value", [$comboboxname, $itemname]); - $this->execute("behat_general::wait_until_exists", [$itemname, "list_item"]); + $this->execute([behat_forms::class, 'set_field_value'], [$comboboxname, $itemname]); + $this->execute([self::class, 'wait_until_exists'], [$itemname, "list_item"]); } return $dropdown; @@ -2619,7 +2619,7 @@ EOF; * @param string $comboboxname The name (label) of the search combo box element. (e.g. "Search users", "Search groups"). */ public function i_confirm_in_search_combobox_exists(string $itemname, string $comboboxname): void { - $this->execute("behat_general::assert_element_contains_text", + $this->execute([self::class, 'assert_element_contains_text'], [$itemname, $this->get_combobox_dropdown_node($comboboxname, $itemname, false), "NodeElement"]); } @@ -2637,7 +2637,7 @@ EOF; * @param string $comboboxname The name (label) of the search combo box element. (e.g. "Search users", "Search groups"). */ public function i_confirm_in_search_combobox_does_not_exist(string $itemname, string $comboboxname): void { - $this->execute("behat_general::assert_element_not_contains_text", + $this->execute([self::class, 'assert_element_not_contains_text'], [$itemname, $this->get_combobox_dropdown_node($comboboxname, $itemname, false), "NodeElement"]); } @@ -2656,11 +2656,11 @@ EOF; */ public function i_click_on_in_search_combobox(string $itemname, string $comboboxname): void { $node = $this->get_combobox_dropdown_node($comboboxname, $itemname); - $this->execute('behat_general::i_click_on_in_the', [ + $this->execute([self::class, 'i_click_on_in_the'], [ $itemname, "list_item", $node, "NodeElement", ]); - $this->execute("behat_general::i_wait_to_be_redirected"); + $this->execute([self::class, 'i_wait_to_be_redirected']); } /**