MOBILE-4061 behat: Treat async calls

main
Pau Ferrer Ocaña 2022-06-14 14:09:26 +02:00
parent 9ce31948ad
commit ca87b084d2
4 changed files with 80 additions and 81 deletions

View File

@ -94,7 +94,7 @@ class behat_app extends behat_app_helper {
public function i_wait_the_app_to_restart() { public function i_wait_the_app_to_restart() {
// Wait window to reload. // Wait window to reload.
$this->spin(function() { $this->spin(function() {
$result = $this->evaluate_script("return !window.behat;"); $result = $this->js("return !window.behat;");
if (!$result) { if (!$result) {
throw new DriverException('Window is not reloading properly.'); throw new DriverException('Window is not reloading properly.');
@ -121,7 +121,7 @@ class behat_app extends behat_app_helper {
$containerName = json_encode($containerName); $containerName = json_encode($containerName);
$this->spin(function() use ($not, $locator, $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') { if ($not && $result === 'OK') {
throw new DriverException('Error, found an item that should not be found'); 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); $locator = $this->parse_element_locator($locator);
$this->spin(function() use ($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') { if ($result !== 'OK') {
throw new DriverException('Error finding item - ' . $result); 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) { public function i_load_more_items_in_the_app(bool $not = false) {
$this->spin(function() use ($not) { $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.') { if ($not && $result !== 'ERROR: All items are already loaded.') {
throw new DriverException('It should not have been possible to load more items'); 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) { public function i_swipe_in_the_app(string $direction) {
$method = 'swipe' . ucwords($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(); $this->wait_for_pending_js();
@ -214,7 +214,7 @@ class behat_app extends behat_app_helper {
$locator = $this->parse_element_locator($locator); $locator = $this->parse_element_locator($locator);
$this->spin(function() use ($locator, $not) { $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) { switch ($result) {
case 'YES': case 'YES':
@ -318,7 +318,7 @@ class behat_app extends behat_app_helper {
$this->login($username); $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') { if ($mycoursesfound !== 'OK') {
// My courses not present enter from Dashboard. // 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) { public function i_press_the_standard_button_in_the_app(string $button) {
$this->spin(function() use ($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') { if ($result !== 'OK') {
throw new DriverException('Error pressing standard button - ' . $result); 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(); $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() { public function i_close_the_popup_in_the_app() {
$this->spin(function() { $this->spin(function() {
$result = $this->evaluate_script("return window.behat.closePopup();"); $result = $this->js("return window.behat.closePopup();");
if ($result !== 'OK') { if ($result !== 'OK') {
throw new DriverException('Error closing popup - ' . $result); throw new DriverException('Error closing popup - ' . $result);
@ -532,7 +532,7 @@ class behat_app extends behat_app_helper {
$locator = $this->parse_element_locator($locator); $locator = $this->parse_element_locator($locator);
$this->spin(function() use ($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') { if ($result !== 'OK') {
throw new DriverException('Error pressing item - ' . $result); 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) { $this->spin(function() use ($selectedtext, $selected, $locator) {
// Don't do anything if the item is already in the expected state. // 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) { if ($result === $selected) {
return true; return true;
} }
// Press item. // Press item.
$result = $this->evaluate_script("return window.behat.press($locator);"); $result = $this->js("return await window.behat.press($locator);");
if ($result !== 'OK') { if ($result !== 'OK') {
throw new DriverException('Error pressing item - ' . $result); throw new DriverException('Error pressing item - ' . $result);
@ -578,7 +578,7 @@ class behat_app extends behat_app_helper {
// Check that it worked as expected. // Check that it worked as expected.
$this->wait_for_pending_js(); $this->wait_for_pending_js();
$result = $this->evaluate_script("return window.behat.isSelected($locator);"); $result = $this->js("return window.behat.isSelected($locator);");
switch ($result) { switch ($result) {
case 'YES': case 'YES':
@ -612,7 +612,7 @@ class behat_app extends behat_app_helper {
$value = addslashes_js($value); $value = addslashes_js($value);
$this->spin(function() use ($field, $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') { if ($result !== 'OK') {
throw new DriverException('Error setting field - ' . $result); 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) { public function the_header_should_be_in_the_app(string $text) {
$this->spin(function() use ($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:') { if (substr($result, 0, 3) !== 'OK:') {
throw new DriverException('Error getting header - ' . $result); 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 * @When I run cron tasks in the app
*/ */
public function 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(); $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 * @When I wait loading to finish in the app
*/ */
public function 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(); $this->wait_for_pending_js();
} }
@ -764,7 +764,7 @@ class behat_app extends behat_app_helper {
$this->getSession()->switchToWindow($names[1]); $this->getSession()->switchToWindow($names[1]);
} }
$this->evaluate_script('window.close();'); $this->js('window.close();');
$this->getSession()->switchToWindow($names[0]); $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 * @throws DriverException If the navigator.online mode is not available
*/ */
public function i_switch_offline_mode(string $offline) { public function i_switch_offline_mode(string $offline) {
$this->evaluate_script("window.behat.network.setForceOffline($offline);"); $this->js("window.behat.network.setForceOffline($offline);");
} }
} }

View File

@ -318,7 +318,7 @@ class behat_app_helper extends behat_base {
$initOptions->skipOnBoarding = $options['skiponboarding'] ?? true; $initOptions->skipOnBoarding = $options['skiponboarding'] ?? true;
$initOptions->configOverrides = $this->appconfig; $initOptions->configOverrides = $this->appconfig;
$this->evaluate_script('window.behatInit(' . json_encode($initOptions) . ');'); $this->js('window.behatInit(' . json_encode($initOptions) . ');');
} catch (Exception $error) { } catch (Exception $error) {
throw new DriverException('Moodle App not running or not running on Automated mode.'); 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 * @param string $script
* @return mixed Resolved promise result. * @return mixed Resolved promise result.
*/ */
protected function evaluate_async_script(string $script) { protected function js(string $script) {
$script = preg_replace('/^return\s+/', '', $script); $scriptnoreturn = preg_replace('/^return\s+/', '', $script);
$script = preg_replace('/;$/', '', $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); $start = microtime(true);
$promisevariable = 'PROMISE_RESULT_' . time(); $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) .then(result => window.$promisevariable = result)
.catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message);"); .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"); throw new DriverException("Async script not resolved after $timeout seconds");
} }
// 0.1 seconds.
usleep(100000); usleep(100000);
} while (!$this->evaluate_script("return '$promisevariable' in window;")); } while (!$this->evaluate_script("return '$promisevariable' in window;"));
@ -514,7 +523,7 @@ class behat_app_helper extends behat_base {
$successXPath = '//page-core-mainmenu'; $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(); $urlscheme = $this->get_mobile_url_scheme();
$url = "$urlscheme://link=" . urlencode($CFG->behat_wwwroot.$path); $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 $customurl To navigate.
* @param string $successXPath The XPath of the element to lookat after navigation. * @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. // 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)) { if (!empty($successXPath)) {
// Wait until the page appears. // 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'); throw new DriverException('Moodle App custom URL page not loaded');
}, false, 30); }, false, 30);
// Wait for JS to finish as well.
$this->wait_for_pending_js();
} }
$this->wait_for_pending_js();
} }
/** /**

View File

@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CorePromisedValue } from '@classes/promised-value';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { NgZone } from '@singletons'; import { NgZone } from '@singletons';
import { TestsBehatBlocking } from './behat-blocking';
import { TestBehatElementLocator } from './behat-runtime'; import { TestBehatElementLocator } from './behat-runtime';
// Containers that block containers behind them. // Containers that block containers behind them.
@ -447,21 +447,23 @@ export class TestsBehatDomUtils {
element.scrollIntoView(false); element.scrollIntoView(false);
return new Promise<DOMRect>((resolve): void => { const promise = new CorePromisedValue<DOMRect>();
requestAnimationFrame(() => {
const rect = element.getBoundingClientRect();
if (initialRect.y !== rect.y) { requestAnimationFrame(() => {
setTimeout(() => { const rect = element.getBoundingClientRect();
resolve(rect);
}, 300);
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<void> { static async pressElement(element: HTMLElement): Promise<void> {
await NgZone.run(async () => { await NgZone.run(async () => {
const blockKey = TestsBehatBlocking.block(); const promise = new CorePromisedValue<void>();
// Events don't bubble up across Shadow DOM boundaries, and some buttons // Events don't bubble up across Shadow DOM boundaries, and some buttons
// may not work without doing this. // may not work without doing this.
@ -501,8 +503,10 @@ export class TestsBehatDomUtils {
element.dispatchEvent(new MouseEvent('mouseup', eventOptions)); element.dispatchEvent(new MouseEvent('mouseup', eventOptions));
element.click(); element.click();
TestsBehatBlocking.unblock(blockKey); promise.resolve();
}, 300); }, 300);
return promise;
}); });
} }
@ -514,7 +518,7 @@ export class TestsBehatDomUtils {
*/ */
static async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> { static async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> {
await NgZone.run(async () => { await NgZone.run(async () => {
const blockKey = TestsBehatBlocking.block(); const promise = new CorePromisedValue<void>();
// Functions to get/set value depending on field type. // Functions to get/set value depending on field type.
const setValue = (text: string) => { const setValue = (text: string) => {
@ -569,7 +573,9 @@ export class TestsBehatDomUtils {
element.dispatchEvent(event); element.dispatchEvent(event);
} }
TestsBehatBlocking.unblock(blockKey); promise.resolve();
return promise;
}); });
} }

View File

@ -83,8 +83,6 @@ export class TestsBehatRuntime {
* @return OK if successful, or ERROR: followed by message. * @return OK if successful, or ERROR: followed by message.
*/ */
static async handleCustomURL(url: string): Promise<string> { static async handleCustomURL(url: string): Promise<string> {
const blockKey = TestsBehatBlocking.block();
try { try {
await NgZone.run(async () => { await NgZone.run(async () => {
await CoreCustomURLSchemes.handleCustomURL(url); await CoreCustomURLSchemes.handleCustomURL(url);
@ -93,8 +91,6 @@ export class TestsBehatRuntime {
return 'OK'; return 'OK';
} catch (error) { } catch (error) {
return 'ERROR: ' + error.message; 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. * @return Promise resolved if all handlers are executed successfully, rejected otherwise.
*/ */
static async forceSyncExecution(): Promise<void> { static async forceSyncExecution(): Promise<void> {
const blockKey = TestsBehatBlocking.block(); await NgZone.run(async () => {
await CoreCronDelegate.forceSyncExecution();
try { });
await NgZone.run(async () => {
await CoreCronDelegate.forceSyncExecution();
});
} finally {
TestsBehatBlocking.unblock(blockKey);
}
} }
/** /**
@ -140,20 +130,13 @@ export class TestsBehatRuntime {
* @return Promise resolved when all components have been rendered. * @return Promise resolved when all components have been rendered.
*/ */
static async waitLoadingToFinish(): Promise<void> { static async waitLoadingToFinish(): Promise<void> {
const blockKey = TestsBehatBlocking.block();
await NgZone.run(async () => { await NgZone.run(async () => {
try { const elements = Array.from(document.body.querySelectorAll<HTMLElement>('core-loading'))
const elements = Array.from(document.body.querySelectorAll<HTMLElement>('core-loading')) .filter((element) => CoreDom.isElementVisible(element));
.filter((element) => CoreDom.isElementVisible(element));
await Promise.all(elements.map(element => await Promise.all(elements.map(element =>
CoreComponentsRegistry.waitComponentReady(element, CoreLoadingComponent))); CoreComponentsRegistry.waitComponentReady(element, CoreLoadingComponent)));
} finally {
TestsBehatBlocking.unblock(blockKey);
}
}); });
} }
/** /**
@ -162,7 +145,7 @@ export class TestsBehatRuntime {
* @param button Type of button to press. * @param button Type of button to press.
* @return OK if successful, or ERROR: followed by message. * @return OK if successful, or ERROR: followed by message.
*/ */
static pressStandard(button: string): string { static async pressStandard(button: string): Promise<string> {
this.log('Action - Click standard button: ' + button); this.log('Action - Click standard button: ' + button);
// Find button // Find button
@ -194,7 +177,7 @@ export class TestsBehatRuntime {
} }
// Click button // Click button
TestsBehatDomUtils.pressElement(foundButton); await TestsBehatDomUtils.pressElement(foundButton);
return 'OK'; return 'OK';
} }
@ -348,7 +331,7 @@ export class TestsBehatRuntime {
* @param locator Element locator. * @param locator Element locator.
* @return OK if successful, or ERROR: followed by message * @return OK if successful, or ERROR: followed by message
*/ */
static press(locator: TestBehatElementLocator): string { static async press(locator: TestBehatElementLocator): Promise<string> {
this.log('Action - Press', locator); this.log('Action - Press', locator);
try { try {
@ -358,7 +341,7 @@ export class TestsBehatRuntime {
return 'ERROR: No element matches locator to press.'; return 'ERROR: No element matches locator to press.';
} }
TestsBehatDomUtils.pressElement(found); await TestsBehatDomUtils.pressElement(found);
return 'OK'; return 'OK';
} catch (error) { } catch (error) {
@ -397,7 +380,7 @@ export class TestsBehatRuntime {
* @param value New value * @param value New value
* @return OK or ERROR: followed by message * @return OK or ERROR: followed by message
*/ */
static setField(field: string, value: string): string { static async setField(field: string, value: string): Promise<string> {
this.log('Action - Set field ' + field + ' to: ' + value); this.log('Action - Set field ' + field + ' to: ' + value);
const found: HTMLElement | HTMLInputElement = TestsBehatDomUtils.findElementBasedOnText( const found: HTMLElement | HTMLInputElement = TestsBehatDomUtils.findElementBasedOnText(
@ -408,7 +391,7 @@ export class TestsBehatRuntime {
return 'ERROR: No element matches field to set.'; return 'ERROR: No element matches field to set.';
} }
TestsBehatDomUtils.setElementValue(found, value); await TestsBehatDomUtils.setElementValue(found, value);
return 'OK'; return 'OK';
} }