From 6d5d1935ea8922fbee89b69bbe41d881dda8b38a Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 17 Jan 2020 09:04:59 +0800 Subject: [PATCH 1/3] MDL-67657 behat: Add wrapper for JS execution --- lib/behat/behat_base.php | 85 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index c97f492b696..4f752ee70db 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -482,12 +482,45 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext { } /** - * Returns whether the scenario is running in a browser that can run Javascript or not. + * Whether Javascript is available in the current Session. * * @return boolean */ protected function running_javascript() { - return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver'; + return self::running_javascript_in_session($this->getSession()); + } + + /** + * Require that javascript be available in the current Session. + * + * @throws DriverException + */ + protected function require_javascript() { + return self::require_javascript_in_session($this->getSession()); + } + + /** + * Whether Javascript is available in the specified Session. + * + * @param Session $session + * @return boolean + */ + protected static function running_javascript_in_session(Session $session): bool { + return get_class($session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver'; + } + + /** + * Require that javascript be available for the specified Session. + * + * @param Session $session + * @throws DriverException + */ + protected static function require_javascript_in_session(Session $session): void { + if (self::running_javascript_in_session($session)) { + return; + } + + throw new DriverException('Javascript is required'); } /** @@ -1206,4 +1239,52 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext { public static function get_named_replacements(): array { return []; } + + /** + * Evaluate the supplied script in the current session, returning the result. + * + * @param string $script + * @return mixed + */ + public function evaluate_script(string $script) { + return self::evaluate_script_in_session($this->getSession(), $script); + } + + /** + * Evaluate the supplied script in the specified session, returning the result. + * + * @param Session $session + * @param string $script + * @return mixed + */ + public static function evaluate_script_in_session(Session $session, string $script) { + self::require_javascript_in_session($session); + + return $session->evaluateScript($script); + } + + /** + * Execute the supplied script in the current session. + * + * No result will be returned. + * + * @param string $script + */ + public function execute_script(string $script): void { + self::execute_script_in_session($this->getSession(), $script); + } + + /** + * Excecute the supplied script in the specified session. + * + * No result will be returned. + * + * @param Session $session + * @param string $script + */ + public static function execute_script_in_session(Session $session, string $script): void { + self::require_javascript_in_session($session); + + $session->executeScript($script); + } } From cd6eadd4078dbe0d4c85f962c1dce1cbc971dc8e Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 16 Jan 2020 17:13:28 +0800 Subject: [PATCH 2/3] MDL-67657 behat: Convert existing uses of executeScript --- lib/behat/behat_base.php | 26 +++++++++++-------- lib/behat/form_field/behat_form_editor.php | 18 +++++++------ .../form_field/behat_form_passwordunmask.php | 12 +++++---- lib/tests/behat/behat_app.php | 18 ++++++------- lib/tests/behat/behat_general.php | 22 ++++++++-------- lib/tests/behat/behat_navigation.php | 2 +- .../behat/behat_assignfeedback_editpdf.php | 6 ++--- .../tests/behat/behat_qtype_ddmarker.php | 6 ++--- search/tests/behat/behat_search.php | 2 +- 9 files changed, 59 insertions(+), 53 deletions(-) diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index 4f752ee70db..56747b2a89c 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -535,7 +535,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext { } // Check on page to see if it's an app page. Safest way is to look for added JavaScript. - return $this->getSession()->evaluateScript('typeof window.behat') === 'object'; + return $this->evaluate_script('return typeof window.behat') === 'object'; } /** @@ -771,14 +771,18 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext { // The window inner height will be as specified, which means the available viewport will // actually be smaller if there is a horizontal scrollbar. We assume that horizontal // scrollbars are rare so this doesn't matter. - $offset = $this->getSession()->getDriver()->evaluateScript( - 'return (function() { var before = document.body.style.overflowY;' . - 'document.body.style.overflowY = "scroll";' . - 'var result = {};' . - 'result.x = window.outerWidth - document.body.offsetWidth;' . - 'result.y = window.outerHeight - window.innerHeight;' . - 'document.body.style.overflowY = before;' . - 'return result; })();'); + $js = <<evaluate_script($js); $width += $offset['x']; $height += $offset['y']; } @@ -827,8 +831,8 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext { } else { return "incomplete" } - }());')); - $pending = $session->evaluateScript($jscode); + })()')); + $pending = self::evaluate_script_in_session($session, $jscode); } catch (NoSuchWindow $nsw) { // We catch an exception here, in case we just closed the window we were interacting with. // No javascript is running if there is no window right? diff --git a/lib/behat/form_field/behat_form_editor.php b/lib/behat/form_field/behat_form_editor.php index 8f3ff721d5d..3d5df09af66 100644 --- a/lib/behat/form_field/behat_form_editor.php +++ b/lib/behat/form_field/behat_form_editor.php @@ -52,14 +52,16 @@ class behat_form_editor extends behat_form_textarea { if ($this->running_javascript()) { $value = addslashes($value); $js = ' -var editor = Y.one(document.getElementById("'.$editorid.'editable")); -if (editor) { - editor.setHTML("' . $value . '"); -} -editor = Y.one(document.getElementById("'.$editorid.'")); -editor.set("value", "' . $value . '"); +(function() { + var editor = Y.one(document.getElementById("'.$editorid.'editable")); + if (editor) { + editor.setHTML("' . $value . '"); + } + editor = Y.one(document.getElementById("'.$editorid.'")); + editor.set("value", "' . $value . '"); +})(); '; - $this->session->executeScript($js); + behat_base::execute_script_in_session($this->session, $js); } else { parent::set_value($value); } @@ -88,7 +90,7 @@ editor.set("value", "' . $value . '"); r.selectNodeContents(e); s.setSingleRange(r); }()); '; - $this->session->executeScript($js); + behat_base::execute_script_in_session($this->session, $js); } /** diff --git a/lib/behat/form_field/behat_form_passwordunmask.php b/lib/behat/form_field/behat_form_passwordunmask.php index 43cf3720d5f..cfc48c26c74 100644 --- a/lib/behat/form_field/behat_form_passwordunmask.php +++ b/lib/behat/form_field/behat_form_passwordunmask.php @@ -51,12 +51,14 @@ class behat_form_passwordunmask extends behat_form_text { if ($this->running_javascript()) { $id = $this->field->getAttribute('id'); $js = <<session->executeScript($js); + behat_base::execute_script_in_session($this->session, $js); } $this->field->setValue($value); diff --git a/lib/tests/behat/behat_app.php b/lib/tests/behat/behat_app.php index af82844ec7d..e474ffc6737 100644 --- a/lib/tests/behat/behat_app.php +++ b/lib/tests/behat/behat_app.php @@ -297,8 +297,7 @@ class behat_app extends behat_base { }, false, 60); // Run the scripts to install Moodle 'pending' checks. - $this->getSession()->executeScript( - file_get_contents(__DIR__ . '/app_behat_runtime.js')); + $this->execute_script(file_get_contents(__DIR__ . '/app_behat_runtime.js')); // Wait until the site login field appears OR the main page. $situation = $this->spin( @@ -373,8 +372,7 @@ class behat_app extends behat_base { */ public function i_press_the_standard_button_in_the_app(string $button) { $this->spin(function($context, $args) use ($button) { - $result = $this->getSession()->evaluateScript('return window.behat.pressStandard("' . - $button . '");'); + $result = $this->evaluate_script("return window.behat.pressStandard('{$button}');"); if ($result !== 'OK') { throw new DriverException('Error pressing standard button - ' . $result); } @@ -391,7 +389,7 @@ class behat_app extends behat_base { */ public function i_close_the_popup_in_the_app() { $this->spin(function($context, $args) { - $result = $this->getSession()->evaluateScript('return window.behat.closePopup();'); + $result = $this->evaluate_script("return window.behat.closePopup();"); if ($result !== 'OK') { throw new DriverException('Error closing popup - ' . $result); } @@ -449,7 +447,7 @@ class behat_app extends behat_base { } else { $nearbit = ''; } - $result = $context->getSession()->evaluateScript('return window.behat.press("' . + $result = $this->evaluate_script('return window.behat.press("' . addslashes_js($text) . '"' . $nearbit .');'); if ($result !== 'OK') { throw new DriverException('Error pressing item - ' . $result); @@ -472,7 +470,7 @@ class behat_app extends behat_base { */ public function i_set_the_field_in_the_app(string $field, string $value) { $this->spin(function($context, $args) use ($field, $value) { - $result = $this->getSession()->evaluateScript('return window.behat.setField("' . + $result = $this->evaluate_script('return window.behat.setField("' . addslashes_js($field) . '", "' . addslashes_js($value) . '");'); if ($result !== 'OK') { throw new DriverException('Error setting field - ' . $result); @@ -494,7 +492,7 @@ class behat_app extends behat_base { */ public function the_header_should_be_in_the_app(string $text) { $result = $this->spin(function($context, $args) { - $result = $this->getSession()->evaluateScript('return window.behat.getHeader();'); + $result = $this->evaluate_script('return window.behat.getHeader();'); if (substr($result, 0, 3) !== 'OK:') { throw new DriverException('Error getting header - ' . $result); } @@ -536,7 +534,7 @@ class behat_app extends behat_base { if (count($names) !== 2) { throw new DriverException('Expected to see 2 tabs open, not ' . count($names)); } - $this->getSession()->getDriver()->executeScript('window.close()'); + $this->execute_script('window.close()'); $this->getSession()->switchToWindow($names[0]); } @@ -548,6 +546,6 @@ class behat_app extends behat_base { * @throws DriverException If the navigator.online mode is not available */ public function i_switch_offline_mode(string $offline) { - $this->getSession()->evaluateScript('appProvider.setForceOffline(' . $offline . ');'); + $this->execute_script('appProvider.setForceOffline(' . $offline . ');'); } } diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index ee6175ae56b..d01ada79b44 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -228,8 +228,7 @@ class behat_general extends behat_base { // unnamed window (presumably the main window) to some other named // window, then we first set the main window name to a conventional // value that we can later use this name to switch back. - $this->getSession()->executeScript( - 'if (window.name == "") window.name = "' . self::MAIN_WINDOW_NAME . '"'); + $this->execute_script('if (window.name == "") window.name = "' . self::MAIN_WINDOW_NAME . '"'); $this->getSession()->switchToWindow($windowname); } @@ -258,7 +257,7 @@ class behat_general extends behat_base { $names = $this->getSession()->getWindowNames(); for ($index = 1; $index < count($names); $index ++) { $this->getSession()->switchToWindow($names[$index]); - $this->getSession()->executeScript("window.open('', '_self').close();"); + $this->execute_script("window.open('', '_self').close();"); } $names = $this->getSession()->getWindowNames(); if (count($names) !== 1) { @@ -924,7 +923,7 @@ class behat_general extends behat_base { return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING; })() EOF; - $ok = $this->getSession()->getDriver()->evaluateScript($js); + $ok = $this->evaluate_script($js); } else { // Using following xpath axe to find it. @@ -1609,11 +1608,12 @@ EOF; $this->pageloaddetectionrunning = true; - $session->executeScript( - 'var span = document.createElement("span"); - span.setAttribute("data-rel", "' . self::PAGE_LOAD_DETECTION_STRING . '"); - span.setAttribute("style", "display: none;"); - document.body.appendChild(span);'); + $this->execute_script( + 'var span = document.createElement("span"); + span.setAttribute("data-rel", "' . self::PAGE_LOAD_DETECTION_STRING . '"); + span.setAttribute("style", "display: none;"); + document.body.appendChild(span);' + ); } /** @@ -1811,7 +1811,7 @@ EOF; $xpath = addslashes_js($element->getXpath()); $script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); '; - $targetisfocused = $this->getSession()->evaluateScript($script); + $targetisfocused = $this->evaluate_script($script); if ($not == ' not') { if ($targetisfocused) { throw new ExpectationException("$nodeelement $nodeselectortype is focused", $this->getSession()); @@ -1843,7 +1843,7 @@ EOF; $xpath = addslashes_js($element->getXpath()); $script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); '; - $targetisfocused = $this->getSession()->evaluateScript($script); + $targetisfocused = $this->evaluate_script($script); if ($not == ' not') { if ($targetisfocused) { throw new ExpectationException("$nodeelement $nodeselectortype is focused", $this->getSession()); diff --git a/lib/tests/behat/behat_navigation.php b/lib/tests/behat/behat_navigation.php index 052fe308ae3..e3035fb8900 100644 --- a/lib/tests/behat/behat_navigation.php +++ b/lib/tests/behat/behat_navigation.php @@ -391,7 +391,7 @@ class behat_navigation extends behat_base { )"; // Adding an extra click we need to show the 'Log in' link. - if (!$this->getSession()->getDriver()->evaluateScript($navbuttonjs)) { + if (!$this->evaluate_script($navbuttonjs)) { return false; } diff --git a/mod/assign/feedback/editpdf/tests/behat/behat_assignfeedback_editpdf.php b/mod/assign/feedback/editpdf/tests/behat/behat_assignfeedback_editpdf.php index fb9f4b2960c..c6c69d686c5 100644 --- a/mod/assign/feedback/editpdf/tests/behat/behat_assignfeedback_editpdf.php +++ b/mod/assign/feedback/editpdf/tests/behat/behat_assignfeedback_editpdf.php @@ -61,21 +61,21 @@ class behat_assignfeedback_editpdf extends behat_base { var event = { clientX: 100, clientY: 250, preventDefault: function() {} }; instance.edit_start(event); }()); '; - $this->getSession()->executeScript($js); + $this->execute_script($js); sleep(1); $js = ' (function() { var instance = M.assignfeedback_editpdf.instance; var event = { clientX: 150, clientY: 275, preventDefault: function() {} }; instance.edit_move(event); }()); '; - $this->getSession()->executeScript($js); + $this->execute_script($js); sleep(1); $js = ' (function() { var instance = M.assignfeedback_editpdf.instance; var event = { clientX: 200, clientY: 300, preventDefault: function() {} }; instance.edit_end(event); }()); '; - $this->getSession()->executeScript($js); + $this->execute_script($js); sleep(1); } diff --git a/question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php b/question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php index 04cabba1853..9a74a1373e5 100644 --- a/question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php +++ b/question/type/ddmarker/tests/behat/behat_qtype_ddmarker.php @@ -68,8 +68,7 @@ class behat_qtype_ddmarker extends behat_base { // DOM node so that its centre is over the centre of anothe DOM node. // Therefore to make it drag to the specified place, we have to add // a target div. - $session = $this->getSession(); - $session->executeScript(" + $this->execute_script(" (function() { if (document.getElementById('target-{$x}-{$y}')) { return; @@ -86,7 +85,8 @@ class behat_qtype_ddmarker extends behat_base { target.style.setProperty('top', yadjusted + 'px'); target.style.setProperty('width', '1px'); target.style.setProperty('height', '1px'); - }())"); + }())" + ); $generalcontext = behat_context_helper::get('behat_general'); $generalcontext->i_drag_and_i_drop_it_in($this->marker_xpath($marker), diff --git a/search/tests/behat/behat_search.php b/search/tests/behat/behat_search.php index c7ca9478f31..ac11c88cf7a 100644 --- a/search/tests/behat/behat_search.php +++ b/search/tests/behat/behat_search.php @@ -51,7 +51,7 @@ class behat_search extends behat_base { $this->execute('behat_forms::i_set_the_field_to', ['q', $query]); // Submit the form. - $this->getSession()->executeScript('document.querySelector(".search-input-form.expanded").submit();'); + $this->execute_script('return document.querySelector(".search-input-form.expanded").submit();'); } /** From 083604ea094b199bddef42fa2e7996fc3e5af699 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Mon, 16 Dec 2019 16:40:52 +0800 Subject: [PATCH 3/3] MDL-67657 behat: Open course with editing using URL This is a huge performance improvement for behat. The current set of steps loads the page It then looks for the Classic/Clean version of the settings menu If it fails to find it looks for the Boost Cog Then it clicks the "Turn editing on" button This can take a substantial period. We do not actually need to run these steps as we are able to jump straight to the URL. We already have access to the sesskey value required to do this. There is not loss in testing functionality because the actual testing of the Turn editing functionality is tested in other places sufficiently. --- lib/behat/behat_base.php | 18 ++++++++++++++++++ lib/tests/behat/behat_navigation.php | 15 ++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index 56747b2a89c..b36272b1424 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -1291,4 +1291,22 @@ EOF; $session->executeScript($script); } + + /** + * Get the session key for the current session via Javascript. + * + * @return string + */ + public function get_sesskey(): string { + $script = <<evaluate_script($script); + } } diff --git a/lib/tests/behat/behat_navigation.php b/lib/tests/behat/behat_navigation.php index e3035fb8900..a2309e4edca 100644 --- a/lib/tests/behat/behat_navigation.php +++ b/lib/tests/behat/behat_navigation.php @@ -757,7 +757,7 @@ class behat_navigation extends behat_base { } /** - * Opens the course homepage with editing mode on. + * Open the course homepage with editing mode enabled. * * @Given /^I am on "(?P(?:[^"]|\\")*)" course homepage with editing mode on$/ * @throws coding_exception @@ -766,9 +766,22 @@ class behat_navigation extends behat_base { */ public function i_am_on_course_homepage_with_editing_mode_on($coursefullname) { global $DB; + $course = $DB->get_record("course", array("fullname" => $coursefullname), 'id', MUST_EXIST); $url = new moodle_url('/course/view.php', ['id' => $course->id]); + + if ($this->running_javascript() && $sesskey = $this->get_sesskey()) { + // Javascript is running so it is possible to grab the session ket and jump straight to editing mode. + $url->param('edit', 1); + $url->param('sesskey', $sesskey); + $this->getSession()->visit($this->locate_path($url->out_as_local_url(false))); + + return; + } + + // Visit the course page. $this->getSession()->visit($this->locate_path($url->out_as_local_url(false))); + try { $this->execute("behat_forms::press_button", get_string('turneditingon')); } catch (Exception $e) {