Source code for bok_choy.javascript

"""
Helpers for dealing with JavaScript synchronization issues.
"""

import functools
import json
from textwrap import dedent
from selenium.common.exceptions import TimeoutException, WebDriverException
from .promise import EmptyPromise


[docs]def js_defined(*js_vars): """ Class decorator that ensures JavaScript variables are defined in the browser. This adds a `wait_for_js` method to the class, which will block until all the expected JavaScript variables are defined. Args: js_vars (list of str): List of JavaScript variable names to wait for. Returns: Decorated class """ return _decorator('_js_vars', js_vars)
[docs]def requirejs(*modules): """ Class decorator that ensures RequireJS modules are loaded in the browser. This adds a `wait_for_js` method to the class, which will block until all the expected RequireJS modules are loaded. Args: modules (list of str) List of RequireJS module names to wait for. Returns: Decorated class """ return _decorator('_requirejs_deps', modules)
[docs]def wait_for_js(function): """ Method decorator that waits for JavaScript dependencies before executing `function`. If the function is not a method, the decorator has no effect. Args: function (callable): Method to decorate. Returns: Decorated method """ @functools.wraps(function) def wrapper(*args, **kwargs): # If not a method, then just call the function if len(args) < 1: return function(*args, **kwargs) # Otherwise, retrieve `self` as the first arg self = args[0] # If the class has been decorated by one of the # JavaScript dependency decorators, it should have # a `wait_for_js` method if hasattr(self, 'wait_for_js'): self.wait_for_js() # Call the function return function(*args, **kwargs) return wrapper
def _decorator(store_name, store_values): """ Return a class decorator that: 1) Defines a new class method, `wait_for_js` 2) Defines a new class list variable, `store_name` and adds `store_values` to the list. """ def decorator(clz): # Add a `wait_for_js` method to the class if not hasattr(clz, 'wait_for_js'): setattr(clz, 'wait_for_js', _wait_for_js) # pylint: disable= literal-used-as-attribute # Store the RequireJS module names in the class if not hasattr(clz, store_name): setattr(clz, store_name, set()) getattr(clz, store_name).update(store_values) return clz return decorator def _wait_for_js(self): """ Class method added by the decorators to allow decorated classes to manually re-check JavaScript dependencies. Expect that `self` is a class that: 1) Has been decorated with either `js_defined` or `requirejs` 2) Has a `browser` property If either (1) or (2) is not satisfied, then do nothing. """ # No Selenium browser available, so return without doing anything if not hasattr(self, 'browser'): return # pylint: disable=protected-access # Wait for JavaScript variables to be defined if hasattr(self, '_js_vars') and self._js_vars: EmptyPromise( lambda: _are_js_vars_defined(self.browser, self._js_vars), f"JavaScript variables defined: {', '.join(self._js_vars)}" ).fulfill() # Wait for RequireJS dependencies to load if hasattr(self, '_requirejs_deps') and self._requirejs_deps: EmptyPromise( lambda: _are_requirejs_deps_loaded(self.browser, self._requirejs_deps), f"RequireJS dependencies loaded: {', '.join(self._requirejs_deps)}", try_limit=5 ).fulfill() def _are_js_vars_defined(browser, js_vars): """ Return a boolean indicating whether all the JavaScript variables `js_vars` are defined on the current page. `browser` is a Selenium webdriver instance. """ # This script will evaluate to True iff all of # the required vars are defined. script = " && ".join([ f"!(typeof {var} === 'undefined')" for var in js_vars ]) try: return browser.execute_script(f"return {script}") except WebDriverException as exc: if "is not defined" in exc.msg or "is undefined" in exc.msg: return False raise def _are_requirejs_deps_loaded(browser, deps): """ Return a boolean indicating whether all the RequireJS dependencies `deps` have loaded on the current page. `browser` is a WebDriver instance. """ # This is a little complicated # # We're going to use `execute_async_script` to give control to # the browser. The browser indicates that it wants to return # control to us by calling `callback`, which is the last item # in the global `arguments` array. # # We install a RequireJS module with the dependencies we want # to ensure are loaded. When our module loads, we return # control to the test suite. script = dedent(""" // Retrieve the callback function used to return control to the test suite var callback = arguments[arguments.length - 1]; // If RequireJS isn't defined, then return immediately if (!window.require) {{ callback("RequireJS not defined"); }} // Otherwise, install a RequireJS module that depends on the modules // we're waiting for. else {{ // Catch errors reported by RequireJS requirejs.onError = callback; // Install our module require({deps}, function() {{ callback('Success'); }}); }} """).format(deps=json.dumps(list(deps))) # Set a timeout to ensure we get control back browser.set_script_timeout(30) # Give control to the browser # `result` will be the argument passed to the callback function try: result = browser.execute_async_script(script) return result == 'Success' except TimeoutException: return False