MOBILE-4110 behat: Clean up js calls
This commit is contained in:
		
							parent
							
								
									c8b16035fe
								
							
						
					
					
						commit
						52259b421f
					
				@ -96,7 +96,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
    public function i_wait_the_app_to_restart() {
 | 
			
		||||
        // Wait window to reload.
 | 
			
		||||
        $this->spin(function() {
 | 
			
		||||
            if ($this->js('window.behat.hasInitialized()')) {
 | 
			
		||||
            if ($this->runtime_js('hasInitialized()')) {
 | 
			
		||||
                // Behat runtime shouldn't be initialized after reload.
 | 
			
		||||
                throw new DriverException('Window is not reloading properly.');
 | 
			
		||||
            }
 | 
			
		||||
@ -114,18 +114,18 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
     * @Then /^I should( not)? find (".+")( inside the .+)? in the app$/
 | 
			
		||||
     * @param bool $not Whether assert that the element was not found
 | 
			
		||||
     * @param string $locator Element locator
 | 
			
		||||
     * @param string $containerName Container name
 | 
			
		||||
     * @param string $container Container name
 | 
			
		||||
     */
 | 
			
		||||
    public function i_find_in_the_app(bool $not, string $locator, string $containerName = '') {
 | 
			
		||||
    public function i_find_in_the_app(bool $not, string $locator, string $container = '') {
 | 
			
		||||
        $locator = $this->parse_element_locator($locator);
 | 
			
		||||
        if (!empty($containerName)) {
 | 
			
		||||
            preg_match('/^ inside the (.+)$/', $containerName, $matches);
 | 
			
		||||
            $containerName = $matches[1];
 | 
			
		||||
        if (!empty($container)) {
 | 
			
		||||
            preg_match('/^ inside the (.+)$/', $container, $matches);
 | 
			
		||||
            $container = $matches[1];
 | 
			
		||||
        }
 | 
			
		||||
        $containerName = json_encode($containerName);
 | 
			
		||||
        $options = json_encode(['containerName' => $container]);
 | 
			
		||||
 | 
			
		||||
        $this->spin(function() use ($not, $locator, $containerName) {
 | 
			
		||||
            $result = $this->js("return window.behat.find($locator, { containerName: $containerName });");
 | 
			
		||||
        $this->spin(function() use ($not, $locator, $options) {
 | 
			
		||||
            $result = $this->runtime_js("find($locator, $options)");
 | 
			
		||||
 | 
			
		||||
            if ($not && $result === 'OK') {
 | 
			
		||||
                throw new DriverException('Error, found an element that should not be found');
 | 
			
		||||
@ -151,7 +151,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
        $locator = $this->parse_element_locator($locator);
 | 
			
		||||
 | 
			
		||||
        $this->spin(function() use ($locator) {
 | 
			
		||||
            $result = $this->js("return window.behat.scrollTo($locator);");
 | 
			
		||||
            $result = $this->runtime_js("scrollTo($locator)");
 | 
			
		||||
 | 
			
		||||
            if ($result !== 'OK') {
 | 
			
		||||
                throw new DriverException('Error finding element - ' . $result);
 | 
			
		||||
@ -174,7 +174,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->js('return await window.behat.loadMoreItems();');
 | 
			
		||||
            $result = $this->runtime_js('loadMoreItems()');
 | 
			
		||||
 | 
			
		||||
            if ($not && $result !== 'ERROR: All items are already loaded.') {
 | 
			
		||||
                throw new DriverException('It should not have been possible to load more items');
 | 
			
		||||
@ -199,7 +199,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
    public function i_swipe_in_the_app(string $direction) {
 | 
			
		||||
        $method = 'swipe' . ucwords($direction);
 | 
			
		||||
 | 
			
		||||
        $this->js("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()");
 | 
			
		||||
        $this->runtime_js("getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()");
 | 
			
		||||
 | 
			
		||||
        $this->wait_for_pending_js();
 | 
			
		||||
 | 
			
		||||
@ -218,7 +218,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
        $locator = $this->parse_element_locator($locator);
 | 
			
		||||
 | 
			
		||||
        $this->spin(function() use ($locator, $not) {
 | 
			
		||||
            $result = $this->js("return window.behat.isSelected($locator);");
 | 
			
		||||
            $result = $this->runtime_js("isSelected($locator)");
 | 
			
		||||
 | 
			
		||||
            switch ($result) {
 | 
			
		||||
                case 'YES':
 | 
			
		||||
@ -325,7 +325,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
            $this->login($username);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $mycoursesfound = $this->js("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});");
 | 
			
		||||
        $mycoursesfound = $this->runtime_js("find({ text: 'My courses', selector: 'ion-tab-button'})");
 | 
			
		||||
 | 
			
		||||
        if ($mycoursesfound !== 'OK') {
 | 
			
		||||
            // My courses not present enter from Dashboard.
 | 
			
		||||
@ -381,7 +381,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->js("return await window.behat.pressStandard('$button');");
 | 
			
		||||
            $result = $this->runtime_js("pressStandard('$button')");
 | 
			
		||||
 | 
			
		||||
            if ($result !== 'OK') {
 | 
			
		||||
                throw new DriverException('Error pressing standard button - ' . $result);
 | 
			
		||||
@ -419,7 +419,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $this->js("window.behat.notificationClicked($notification)");
 | 
			
		||||
        $this->zone_js("pushNotifications.notificationClicked($notification)", true);
 | 
			
		||||
        $this->wait_for_pending_js();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -507,7 +507,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
     */
 | 
			
		||||
    public function i_close_the_popup_in_the_app() {
 | 
			
		||||
        $this->spin(function()  {
 | 
			
		||||
            $result = $this->js("return window.behat.closePopup();");
 | 
			
		||||
            $result = $this->runtime_js('closePopup()');
 | 
			
		||||
 | 
			
		||||
            if ($result !== 'OK') {
 | 
			
		||||
                throw new DriverException('Error closing popup - ' . $result);
 | 
			
		||||
@ -545,7 +545,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
        $locator = $this->parse_element_locator($locator);
 | 
			
		||||
 | 
			
		||||
        $this->spin(function() use ($locator) {
 | 
			
		||||
            $result = $this->js("return await window.behat.press($locator);");
 | 
			
		||||
            $result = $this->runtime_js("press($locator)");
 | 
			
		||||
 | 
			
		||||
            if ($result !== 'OK') {
 | 
			
		||||
                throw new DriverException('Error pressing item - ' . $result);
 | 
			
		||||
@ -588,7 +588,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
        $locator = $this->parse_element_locator($locator);
 | 
			
		||||
 | 
			
		||||
        $this->spin(function() use ($not, $locator) {
 | 
			
		||||
            $result = $this->js("return window.behat.find($locator, { onlyClickable: true });");
 | 
			
		||||
            $result = $this->runtime_js("find($locator, { onlyClickable: true })");
 | 
			
		||||
 | 
			
		||||
            if ($not && $result === 'OK') {
 | 
			
		||||
                throw new DriverException('Error, found a clickable element that should not be found');
 | 
			
		||||
@ -622,14 +622,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->js("return window.behat.isSelected($locator);");
 | 
			
		||||
            $result = $this->runtime_js("isSelected($locator)");
 | 
			
		||||
 | 
			
		||||
            if ($result === $selected) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Press element.
 | 
			
		||||
            $result = $this->js("return await window.behat.press($locator);");
 | 
			
		||||
            $result = $this->runtime_js("press($locator)");
 | 
			
		||||
 | 
			
		||||
            if ($result !== 'OK') {
 | 
			
		||||
                throw new DriverException('Error pressing element - ' . $result);
 | 
			
		||||
@ -638,7 +638,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
            // Check that it worked as expected.
 | 
			
		||||
            $this->wait_for_pending_js();
 | 
			
		||||
 | 
			
		||||
            $result = $this->js("return window.behat.isSelected($locator);");
 | 
			
		||||
            $result = $this->runtime_js("isSelected($locator)");
 | 
			
		||||
 | 
			
		||||
            switch ($result) {
 | 
			
		||||
                case 'YES':
 | 
			
		||||
@ -672,7 +672,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
        $value = addslashes_js($value);
 | 
			
		||||
 | 
			
		||||
        $this->spin(function() use ($field, $value) {
 | 
			
		||||
            $result = $this->js("return await window.behat.setField(\"$field\", \"$value\");");
 | 
			
		||||
            $result = $this->runtime_js("setField('$field', '$value')");
 | 
			
		||||
 | 
			
		||||
            if ($result !== 'OK') {
 | 
			
		||||
                throw new DriverException('Error setting field - ' . $result);
 | 
			
		||||
@ -711,7 +711,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->js('return window.behat.getHeader();');
 | 
			
		||||
            $result = $this->runtime_js('getHeader()');
 | 
			
		||||
 | 
			
		||||
            if (substr($result, 0, 3) !== 'OK:') {
 | 
			
		||||
                throw new DriverException('Error getting header - ' . $result);
 | 
			
		||||
@ -792,7 +792,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->js('await window.behat.forceSyncExecution()');
 | 
			
		||||
        $this->zone_js('cronDelegate.forceSyncExecution()');
 | 
			
		||||
        $this->wait_for_pending_js();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -802,7 +802,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->js('await window.behat.waitLoadingToFinish()');
 | 
			
		||||
        $this->runtime_js('waitLoadingToFinish()');
 | 
			
		||||
        $this->wait_for_pending_js();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -824,7 +824,7 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
            $this->getSession()->switchToWindow($names[1]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->js('window.close();');
 | 
			
		||||
        $this->js('window.close()');
 | 
			
		||||
        $this->getSession()->switchToWindow($names[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -836,7 +836,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->js("window.behat.network.setForceOffline($offline);");
 | 
			
		||||
        $this->runtime_js("network.setForceOffline($offline)");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -313,12 +313,12 @@ class behat_app_helper extends behat_base {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            // Init Behat JavaScript runtime.
 | 
			
		||||
            $initoptions = json_encode([
 | 
			
		||||
                'skipOnBoarding' => $options['skiponboarding'] ?? true,
 | 
			
		||||
                'configOverrides' => $this->appconfig,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            $initOptions = new StdClass();
 | 
			
		||||
            $initOptions->skipOnBoarding = $options['skiponboarding'] ?? true;
 | 
			
		||||
            $initOptions->configOverrides = $this->appconfig;
 | 
			
		||||
 | 
			
		||||
            $this->js('window.behat.init(' . json_encode($initOptions) . ');');
 | 
			
		||||
            $this->runtime_js("init($initoptions)");
 | 
			
		||||
        } catch (Exception $error) {
 | 
			
		||||
            throw new DriverException('Moodle App not running or not running on Automated mode.');
 | 
			
		||||
        }
 | 
			
		||||
@ -456,7 +456,7 @@ class behat_app_helper extends behat_base {
 | 
			
		||||
 | 
			
		||||
        $res = $this->evaluate_script("Promise.resolve($script)
 | 
			
		||||
            .then(result => window.$promisevariable = result)
 | 
			
		||||
            .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message);");
 | 
			
		||||
            .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message)");
 | 
			
		||||
 | 
			
		||||
        do {
 | 
			
		||||
            if (microtime(true) - $start > $timeout) {
 | 
			
		||||
@ -465,15 +465,42 @@ class behat_app_helper extends behat_base {
 | 
			
		||||
 | 
			
		||||
            // 0.1 seconds.
 | 
			
		||||
            usleep(100000);
 | 
			
		||||
        } while (!$this->evaluate_script("return '$promisevariable' in window;"));
 | 
			
		||||
        } while (!$this->evaluate_script("'$promisevariable' in window"));
 | 
			
		||||
 | 
			
		||||
        $result = $this->evaluate_script("return window.$promisevariable;");
 | 
			
		||||
        $result = $this->evaluate_script("window.$promisevariable");
 | 
			
		||||
 | 
			
		||||
        $this->evaluate_script("delete window.$promisevariable;");
 | 
			
		||||
        $this->evaluate_script("delete window.$promisevariable");
 | 
			
		||||
 | 
			
		||||
        if (is_string($result) && strrpos($result, 'Async code rejected:') === 0) {
 | 
			
		||||
            throw new DriverException($result);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Evaluate and execute methods from the Behat runtime.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $script
 | 
			
		||||
     * @return mixed Result.
 | 
			
		||||
     */
 | 
			
		||||
    protected function runtime_js(string $script) {
 | 
			
		||||
        return $this->js("window.behat.$script");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Evaluate and execute methods from the Behat runtime inside the Angular zone.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $script
 | 
			
		||||
     * @param bool $blocking
 | 
			
		||||
     * @return mixed Result.
 | 
			
		||||
     */
 | 
			
		||||
    protected function zone_js(string $script, bool $blocking = false) {
 | 
			
		||||
        $blockingjson = json_encode($blocking);
 | 
			
		||||
 | 
			
		||||
        return $this->runtime_js("runInZone(() => window.behat.$script, $blockingjson)");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Opens a custom URL for automatic login and redirect from the Moodle App (and waits to finish.)
 | 
			
		||||
     *
 | 
			
		||||
@ -548,8 +575,7 @@ class behat_app_helper extends behat_base {
 | 
			
		||||
     * @param string $successXPath The XPath of the element to lookat after navigation.
 | 
			
		||||
     */
 | 
			
		||||
    protected function handle_url(string $customurl, string $successXPath = '') {
 | 
			
		||||
        // Instead of using evaluate_async_script, we wait for the path to load.
 | 
			
		||||
        $result = $this->js("return await window.behat.handleCustomURL('$customurl');");
 | 
			
		||||
        $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')");
 | 
			
		||||
 | 
			
		||||
        if ($result !== 'OK') {
 | 
			
		||||
            throw new DriverException('Error handling url - ' . $result);
 | 
			
		||||
 | 
			
		||||
@ -178,7 +178,7 @@ class performance_measure implements behat_app_listener {
 | 
			
		||||
     * @return int Current time in milliseconds.
 | 
			
		||||
     */
 | 
			
		||||
    private function now(): int {
 | 
			
		||||
        return $this->driver->evaluateScript('Date.now();');
 | 
			
		||||
        return $this->driver->evaluateScript('Date.now()');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -14,17 +14,14 @@
 | 
			
		||||
 | 
			
		||||
import { TestingBehatDomUtils } from './behat-dom';
 | 
			
		||||
import { TestingBehatBlocking } from './behat-blocking';
 | 
			
		||||
import { CoreCustomURLSchemes } from '@services/urlschemes';
 | 
			
		||||
import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes';
 | 
			
		||||
import { CoreLoginHelperProvider } from '@features/login/services/login-helper';
 | 
			
		||||
import { CoreConfig } from '@services/config';
 | 
			
		||||
import { EnvironmentConfig } from '@/types/config';
 | 
			
		||||
import { makeSingleton, NgZone } from '@singletons';
 | 
			
		||||
import { CoreNetwork, CoreNetworkService } from '@services/network';
 | 
			
		||||
import {
 | 
			
		||||
    CorePushNotifications,
 | 
			
		||||
    CorePushNotificationsNotificationBasicData,
 | 
			
		||||
} from '@features/pushnotifications/services/pushnotifications';
 | 
			
		||||
import { CoreCronDelegate } from '@services/cron';
 | 
			
		||||
import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications';
 | 
			
		||||
import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron';
 | 
			
		||||
import { CoreLoadingComponent } from '@components/loading/loading';
 | 
			
		||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
 | 
			
		||||
import { CoreDom } from '@singletons/dom';
 | 
			
		||||
@ -40,10 +37,22 @@ export class TestingBehatRuntimeService {
 | 
			
		||||
 | 
			
		||||
    protected initialized = false;
 | 
			
		||||
 | 
			
		||||
    get cronDelegate(): CoreCronDelegateService {
 | 
			
		||||
        return CoreCronDelegate.instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get customUrlSchemes(): CoreCustomURLSchemesProvider {
 | 
			
		||||
        return CoreCustomURLSchemes.instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get network(): CoreNetworkService {
 | 
			
		||||
        return CoreNetwork.instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get pushNotifications(): CorePushNotificationsProvider {
 | 
			
		||||
        return CorePushNotifications.instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Init behat functions and set options like skipping onboarding.
 | 
			
		||||
     *
 | 
			
		||||
@ -78,53 +87,25 @@ export class TestingBehatRuntimeService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handles a custom URL.
 | 
			
		||||
     * Run an operation inside the angular zone and return result.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url Url to open.
 | 
			
		||||
     * @param operation Operation callback.
 | 
			
		||||
     * @return OK if successful, or ERROR: followed by message.
 | 
			
		||||
     */
 | 
			
		||||
    async handleCustomURL(url: string): Promise<string> {
 | 
			
		||||
    async runInZone(operation: () => unknown, blocking: boolean = false): Promise<string> {
 | 
			
		||||
        const blockKey = blocking && TestingBehatBlocking.block();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await NgZone.run(async () => {
 | 
			
		||||
                await CoreCustomURLSchemes.handleCustomURL(url);
 | 
			
		||||
            });
 | 
			
		||||
            await NgZone.run(operation);
 | 
			
		||||
 | 
			
		||||
            return 'OK';
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            return 'ERROR: ' + error.message;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function called when a push notification is clicked. Redirect the user to the right state.
 | 
			
		||||
     *
 | 
			
		||||
     * @param data Notification data.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise<void> {
 | 
			
		||||
        const blockKey = TestingBehatBlocking.block();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await NgZone.run(async () => {
 | 
			
		||||
                await CorePushNotifications.notificationClicked(data);
 | 
			
		||||
            });
 | 
			
		||||
        } finally {
 | 
			
		||||
            TestingBehatBlocking.unblock(blockKey);
 | 
			
		||||
            blockKey && TestingBehatBlocking.unblock(blockKey);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Force execution of synchronization cron tasks without waiting for the scheduled time.
 | 
			
		||||
     * Please notice that some tasks may not be executed depending on the network connection and sync settings.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved if all handlers are executed successfully, rejected otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    async forceSyncExecution(): Promise<void> {
 | 
			
		||||
        await NgZone.run(async () => {
 | 
			
		||||
            await CoreCronDelegate.forceSyncExecution();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wait all controlled components to be rendered.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user