diff --git a/local-moodleappbehat/tests/behat/behat_app.php b/local-moodleappbehat/tests/behat/behat_app.php index f848ed271..b21f80134 100644 --- a/local-moodleappbehat/tests/behat/behat_app.php +++ b/local-moodleappbehat/tests/behat/behat_app.php @@ -94,7 +94,7 @@ class behat_app extends behat_app_helper { public function i_wait_the_app_to_restart() { // Wait window to reload. $this->spin(function() { - $result = $this->evaluate_script("return !window.behat;"); + $result = $this->js("return !window.behat;"); if (!$result) { throw new DriverException('Window is not reloading properly.'); @@ -121,7 +121,7 @@ class behat_app extends behat_app_helper { $containerName = json_encode($containerName); $this->spin(function() use ($not, $locator, $containerName) { - $result = $this->evaluate_script("return window.behat.find($locator, $containerName);"); + $result = $this->js("return window.behat.find($locator, $containerName);"); if ($not && $result === 'OK') { throw new DriverException('Error, found an item that should not be found'); @@ -147,7 +147,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator) { - $result = $this->evaluate_script("return window.behat.scrollTo($locator);"); + $result = $this->js("return window.behat.scrollTo($locator);"); if ($result !== 'OK') { throw new DriverException('Error finding item - ' . $result); @@ -170,7 +170,7 @@ class behat_app extends behat_app_helper { */ public function i_load_more_items_in_the_app(bool $not = false) { $this->spin(function() use ($not) { - $result = $this->evaluate_async_script('return window.behat.loadMoreItems();'); + $result = $this->js('return await window.behat.loadMoreItems();'); if ($not && $result !== 'ERROR: All items are already loaded.') { throw new DriverException('It should not have been possible to load more items'); @@ -195,7 +195,7 @@ class behat_app extends behat_app_helper { public function i_swipe_in_the_app(string $direction) { $method = 'swipe' . ucwords($direction); - $this->evaluate_script("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); + $this->js("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); $this->wait_for_pending_js(); @@ -214,7 +214,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator, $not) { - $result = $this->evaluate_script("return window.behat.isSelected($locator);"); + $result = $this->js("return window.behat.isSelected($locator);"); switch ($result) { case 'YES': @@ -318,7 +318,7 @@ class behat_app extends behat_app_helper { $this->login($username); } - $mycoursesfound = $this->evaluate_script("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});"); + $mycoursesfound = $this->js("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});"); if ($mycoursesfound !== 'OK') { // My courses not present enter from Dashboard. @@ -370,7 +370,7 @@ class behat_app extends behat_app_helper { */ public function i_press_the_standard_button_in_the_app(string $button) { $this->spin(function() use ($button) { - $result = $this->evaluate_script("return window.behat.pressStandard('$button');"); + $result = $this->js("return await window.behat.pressStandard('$button');"); if ($result !== 'OK') { throw new DriverException('Error pressing standard button - ' . $result); @@ -408,7 +408,7 @@ class behat_app extends behat_app_helper { ], ]); - $this->evaluate_script("window.behat.notificationClicked($notification)"); + $this->js("window.behat.notificationClicked($notification)"); $this->wait_for_pending_js(); } @@ -494,7 +494,7 @@ class behat_app extends behat_app_helper { */ public function i_close_the_popup_in_the_app() { $this->spin(function() { - $result = $this->evaluate_script("return window.behat.closePopup();"); + $result = $this->js("return window.behat.closePopup();"); if ($result !== 'OK') { throw new DriverException('Error closing popup - ' . $result); @@ -532,7 +532,7 @@ class behat_app extends behat_app_helper { $locator = $this->parse_element_locator($locator); $this->spin(function() use ($locator) { - $result = $this->evaluate_script("return window.behat.press($locator);"); + $result = $this->js("return await window.behat.press($locator);"); if ($result !== 'OK') { throw new DriverException('Error pressing item - ' . $result); @@ -562,14 +562,14 @@ class behat_app extends behat_app_helper { $this->spin(function() use ($selectedtext, $selected, $locator) { // Don't do anything if the item is already in the expected state. - $result = $this->evaluate_script("return window.behat.isSelected($locator);"); + $result = $this->js("return window.behat.isSelected($locator);"); if ($result === $selected) { return true; } // Press item. - $result = $this->evaluate_script("return window.behat.press($locator);"); + $result = $this->js("return await window.behat.press($locator);"); if ($result !== 'OK') { throw new DriverException('Error pressing item - ' . $result); @@ -578,7 +578,7 @@ class behat_app extends behat_app_helper { // Check that it worked as expected. $this->wait_for_pending_js(); - $result = $this->evaluate_script("return window.behat.isSelected($locator);"); + $result = $this->js("return window.behat.isSelected($locator);"); switch ($result) { case 'YES': @@ -612,7 +612,7 @@ class behat_app extends behat_app_helper { $value = addslashes_js($value); $this->spin(function() use ($field, $value) { - $result = $this->evaluate_script("return window.behat.setField(\"$field\", \"$value\");"); + $result = $this->js("return await window.behat.setField(\"$field\", \"$value\");"); if ($result !== 'OK') { throw new DriverException('Error setting field - ' . $result); @@ -651,7 +651,7 @@ class behat_app extends behat_app_helper { */ public function the_header_should_be_in_the_app(string $text) { $this->spin(function() use ($text) { - $result = $this->evaluate_script('return window.behat.getHeader();'); + $result = $this->js('return window.behat.getHeader();'); if (substr($result, 0, 3) !== 'OK:') { throw new DriverException('Error getting header - ' . $result); @@ -732,7 +732,7 @@ class behat_app extends behat_app_helper { * @When I run cron tasks in the app */ public function i_run_cron_tasks_in_the_app() { - $this->evaluate_script('window.behat.forceSyncExecution()'); + $this->js('await window.behat.forceSyncExecution()'); $this->wait_for_pending_js(); } @@ -742,7 +742,7 @@ class behat_app extends behat_app_helper { * @When I wait loading to finish in the app */ public function i_wait_loading_to_finish_in_the_app() { - $this->evaluate_script('window.behat.waitLoadingToFinish()'); + $this->js('await window.behat.waitLoadingToFinish()'); $this->wait_for_pending_js(); } @@ -764,7 +764,7 @@ class behat_app extends behat_app_helper { $this->getSession()->switchToWindow($names[1]); } - $this->evaluate_script('window.close();'); + $this->js('window.close();'); $this->getSession()->switchToWindow($names[0]); } @@ -776,7 +776,7 @@ class behat_app extends behat_app_helper { * @throws DriverException If the navigator.online mode is not available */ public function i_switch_offline_mode(string $offline) { - $this->evaluate_script("window.behat.network.setForceOffline($offline);"); + $this->js("window.behat.network.setForceOffline($offline);"); } } diff --git a/local-moodleappbehat/tests/behat/behat_app_helper.php b/local-moodleappbehat/tests/behat/behat_app_helper.php index 52f3c287d..b929e3402 100644 --- a/local-moodleappbehat/tests/behat/behat_app_helper.php +++ b/local-moodleappbehat/tests/behat/behat_app_helper.php @@ -318,7 +318,7 @@ class behat_app_helper extends behat_base { $initOptions->skipOnBoarding = $options['skiponboarding'] ?? true; $initOptions->configOverrides = $this->appconfig; - $this->evaluate_script('window.behatInit(' . json_encode($initOptions) . ');'); + $this->js('window.behatInit(' . json_encode($initOptions) . ');'); } catch (Exception $error) { throw new DriverException('Moodle App not running or not running on Automated mode.'); } @@ -434,19 +434,27 @@ class behat_app_helper extends behat_base { } /** - * Evaluate a script that returns a Promise. + * Evaluate and execute scripts checking for promises if needed. * * @param string $script * @return mixed Resolved promise result. */ - protected function evaluate_async_script(string $script) { - $script = preg_replace('/^return\s+/', '', $script); - $script = preg_replace('/;$/', '', $script); + protected function js(string $script) { + $scriptnoreturn = preg_replace('/^return\s+/', '', $script); + $scriptnoreturn = preg_replace('/;$/', '', $scriptnoreturn); + + if (!preg_match('/^await\s+/', $scriptnoreturn)) { + // No async. + return $this->evaluate_script($script); + } + + $script = preg_replace('/^await\s+/', '', $scriptnoreturn); + $start = microtime(true); $promisevariable = 'PROMISE_RESULT_' . time(); - $timeout = self::get_timeout(); + $timeout = self::get_extended_timeout(); - $this->evaluate_script("Promise.resolve($script) + $res = $this->evaluate_script("Promise.resolve($script) .then(result => window.$promisevariable = result) .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message);"); @@ -455,6 +463,7 @@ class behat_app_helper extends behat_base { throw new DriverException("Async script not resolved after $timeout seconds"); } + // 0.1 seconds. usleep(100000); } while (!$this->evaluate_script("return '$promisevariable' in window;")); @@ -514,7 +523,7 @@ class behat_app_helper extends behat_base { $successXPath = '//page-core-mainmenu'; } - $this->handle_url_and_wait_page_to_load($url, $successXPath); + $this->handle_url($url, $successXPath); } /** @@ -529,7 +538,7 @@ class behat_app_helper extends behat_base { $urlscheme = $this->get_mobile_url_scheme(); $url = "$urlscheme://link=" . urlencode($CFG->behat_wwwroot.$path); - $this->handle_url_and_wait_page_to_load($url); + $this->handle_url($url); } /** @@ -538,11 +547,13 @@ class behat_app_helper extends behat_base { * @param string $customurl To navigate. * @param string $successXPath The XPath of the element to lookat after navigation. */ - protected function handle_url_and_wait_page_to_load(string $customurl, string $successXPath = '') { + protected function handle_url(string $customurl, string $successXPath = '') { // Instead of using evaluate_async_script, we wait for the path to load. - $this->evaluate_script("return window.behat.handleCustomURL('$customurl')"); + $result = $this->js("return await window.behat.handleCustomURL('$customurl');"); - $this->wait_for_pending_js(); + if ($result !== 'OK') { + throw new DriverException('Error handling url - ' . $result); + } if (!empty($successXPath)) { // Wait until the page appears. @@ -554,10 +565,9 @@ class behat_app_helper extends behat_base { } throw new DriverException('Moodle App custom URL page not loaded'); }, false, 30); - - // Wait for JS to finish as well. - $this->wait_for_pending_js(); } + + $this->wait_for_pending_js(); } /** diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index 7e2334d78..1292f231a 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CorePromisedValue } from '@classes/promised-value'; import { CoreUtils } from '@services/utils/utils'; import { NgZone } from '@singletons'; -import { TestsBehatBlocking } from './behat-blocking'; import { TestBehatElementLocator } from './behat-runtime'; // Containers that block containers behind them. @@ -447,21 +447,23 @@ export class TestsBehatDomUtils { element.scrollIntoView(false); - return new Promise((resolve): void => { - requestAnimationFrame(() => { - const rect = element.getBoundingClientRect(); + const promise = new CorePromisedValue(); - if (initialRect.y !== rect.y) { - setTimeout(() => { - resolve(rect); - }, 300); + requestAnimationFrame(() => { + const rect = element.getBoundingClientRect(); - return; - } + if (initialRect.y !== rect.y) { + setTimeout(() => { + promise.resolve(rect); + }, 300); - resolve(rect); - }); + return; + } + + promise.resolve(rect); }); + + return promise; }; /** @@ -471,7 +473,7 @@ export class TestsBehatDomUtils { */ static async pressElement(element: HTMLElement): Promise { await NgZone.run(async () => { - const blockKey = TestsBehatBlocking.block(); + const promise = new CorePromisedValue(); // Events don't bubble up across Shadow DOM boundaries, and some buttons // may not work without doing this. @@ -501,8 +503,10 @@ export class TestsBehatDomUtils { element.dispatchEvent(new MouseEvent('mouseup', eventOptions)); element.click(); - TestsBehatBlocking.unblock(blockKey); + promise.resolve(); }, 300); + + return promise; }); } @@ -514,7 +518,7 @@ export class TestsBehatDomUtils { */ static async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise { await NgZone.run(async () => { - const blockKey = TestsBehatBlocking.block(); + const promise = new CorePromisedValue(); // Functions to get/set value depending on field type. const setValue = (text: string) => { @@ -569,7 +573,9 @@ export class TestsBehatDomUtils { element.dispatchEvent(event); } - TestsBehatBlocking.unblock(blockKey); + promise.resolve(); + + return promise; }); } diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index 95b826c5c..c99c0b996 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -83,8 +83,6 @@ export class TestsBehatRuntime { * @return OK if successful, or ERROR: followed by message. */ static async handleCustomURL(url: string): Promise { - const blockKey = TestsBehatBlocking.block(); - try { await NgZone.run(async () => { await CoreCustomURLSchemes.handleCustomURL(url); @@ -93,8 +91,6 @@ export class TestsBehatRuntime { return 'OK'; } catch (error) { return 'ERROR: ' + error.message; - } finally { - TestsBehatBlocking.unblock(blockKey); } } @@ -123,15 +119,9 @@ export class TestsBehatRuntime { * @return Promise resolved if all handlers are executed successfully, rejected otherwise. */ static async forceSyncExecution(): Promise { - const blockKey = TestsBehatBlocking.block(); - - try { - await NgZone.run(async () => { - await CoreCronDelegate.forceSyncExecution(); - }); - } finally { - TestsBehatBlocking.unblock(blockKey); - } + await NgZone.run(async () => { + await CoreCronDelegate.forceSyncExecution(); + }); } /** @@ -140,20 +130,13 @@ export class TestsBehatRuntime { * @return Promise resolved when all components have been rendered. */ static async waitLoadingToFinish(): Promise { - const blockKey = TestsBehatBlocking.block(); - await NgZone.run(async () => { - try { - const elements = Array.from(document.body.querySelectorAll('core-loading')) - .filter((element) => CoreDom.isElementVisible(element)); + const elements = Array.from(document.body.querySelectorAll('core-loading')) + .filter((element) => CoreDom.isElementVisible(element)); - await Promise.all(elements.map(element => - CoreComponentsRegistry.waitComponentReady(element, CoreLoadingComponent))); - } finally { - TestsBehatBlocking.unblock(blockKey); - } + await Promise.all(elements.map(element => + CoreComponentsRegistry.waitComponentReady(element, CoreLoadingComponent))); }); - } /** @@ -162,7 +145,7 @@ export class TestsBehatRuntime { * @param button Type of button to press. * @return OK if successful, or ERROR: followed by message. */ - static pressStandard(button: string): string { + static async pressStandard(button: string): Promise { this.log('Action - Click standard button: ' + button); // Find button @@ -194,7 +177,7 @@ export class TestsBehatRuntime { } // Click button - TestsBehatDomUtils.pressElement(foundButton); + await TestsBehatDomUtils.pressElement(foundButton); return 'OK'; } @@ -348,7 +331,7 @@ export class TestsBehatRuntime { * @param locator Element locator. * @return OK if successful, or ERROR: followed by message */ - static press(locator: TestBehatElementLocator): string { + static async press(locator: TestBehatElementLocator): Promise { this.log('Action - Press', locator); try { @@ -358,7 +341,7 @@ export class TestsBehatRuntime { return 'ERROR: No element matches locator to press.'; } - TestsBehatDomUtils.pressElement(found); + await TestsBehatDomUtils.pressElement(found); return 'OK'; } catch (error) { @@ -397,7 +380,7 @@ export class TestsBehatRuntime { * @param value New value * @return OK or ERROR: followed by message */ - static setField(field: string, value: string): string { + static async setField(field: string, value: string): Promise { this.log('Action - Set field ' + field + ' to: ' + value); const found: HTMLElement | HTMLInputElement = TestsBehatDomUtils.findElementBasedOnText( @@ -408,7 +391,7 @@ export class TestsBehatRuntime { return 'ERROR: No element matches field to set.'; } - TestsBehatDomUtils.setElementValue(found, value); + await TestsBehatDomUtils.setElementValue(found, value); return 'OK'; }