diff --git a/asl_rulebook2/webapp/__init__.py b/asl_rulebook2/webapp/__init__.py index 19b3120..f70be0a 100644 --- a/asl_rulebook2/webapp/__init__.py +++ b/asl_rulebook2/webapp/__init__.py @@ -69,6 +69,7 @@ else: # load the application import asl_rulebook2.webapp.main #pylint: disable=wrong-import-position,cyclic-import +import asl_rulebook2.webapp.startup #pylint: disable=wrong-import-position,cyclic-import import asl_rulebook2.webapp.content #pylint: disable=wrong-import-position,cyclic-import from asl_rulebook2.webapp import globvars #pylint: disable=wrong-import-position,cyclic-import app.before_request( globvars.on_request ) diff --git a/asl_rulebook2/webapp/content.py b/asl_rulebook2/webapp/content.py index 0d020cf..afcc008 100644 --- a/asl_rulebook2/webapp/content.py +++ b/asl_rulebook2/webapp/content.py @@ -14,7 +14,7 @@ content_docs = None # --------------------------------------------------------------------- -def load_content_docs( logger ): +def load_content_docs( startup_msgs, logger ): """Load the content documents from the data directory.""" # initialize @@ -23,27 +23,36 @@ def load_content_docs( logger ): dname = app.config.get( "DATA_DIR" ) if not dname: return - if not os.path.dirname( dname ): - raise RuntimeError( "Invalid data directory: {}".format( dname ) ) + if not os.path.isdir( dname ): + startup_msgs.error( "Invalid data directory.", dname ) + return - def get_doc( content_doc, key, fname, binary=False ): + def load_file( fname, content_doc, key, on_error, binary=False ): fname = os.path.join( dname, fname ) if not os.path.isfile( fname ): - return - if binary: - with open( fname, mode="rb" ) as fp: - data = fp.read() - logger.debug( "- Loaded \"%s\" file: #bytes=%d", key, len(data) ) - content_doc[ key ] = data - else: - with open( fname, "r", encoding="utf-8" ) as fp: - content_doc[ key ] = json.load( fp ) - logger.debug( "- Loaded \"%s\" file.", key ) + return False + # load the specified file + try: + if binary: + with open( fname, mode="rb" ) as fp: + data = fp.read() + logger.debug( "- Loaded \"%s\" file: #bytes=%d", key, len(data) ) + else: + with open( fname, "r", encoding="utf-8" ) as fp: + data = json.load( fp ) + logger.debug( "- Loaded \"%s\" file.", key ) + except Exception as ex: #pylint: disable=broad-except + on_error( "Couldn't load \"{}\".".format( os.path.basename(fname) ), str(ex) ) + return False + # save the file data + content_doc[ key ] = data + return True # load each content doc logger.info( "Loading content docs: %s", dname ) fspec = os.path.join( dname, "*.index" ) for fname in glob.glob( fspec ): + # load the main index file fname2 = os.path.basename( fname ) logger.info( "- %s", fname2 ) title = os.path.splitext( fname2 )[0] @@ -52,10 +61,13 @@ def load_content_docs( logger ): "doc_id": slugify( title ), "title": title, } - get_doc( content_doc, "index", fname2 ) - get_doc( content_doc, "targets", change_extn(fname2,".targets") ) - get_doc( content_doc, "footnotes", change_extn(fname2,".footnotes") ) - get_doc( content_doc, "content", change_extn(fname2,".pdf"), binary=True ) + if not load_file( fname2, content_doc, "index", startup_msgs.error ): + continue # nb: we can't do anything without an index file + # load any associated files + load_file( change_extn(fname2,".targets"), content_doc, "targets", startup_msgs.warning ) + load_file( change_extn(fname2,".footnotes"), content_doc, "footnotes", startup_msgs.warning ) + load_file( change_extn(fname2,".pdf"), content_doc, "content", startup_msgs.warning, binary=True ) + # save the new content doc content_docs[ content_doc["doc_id"] ] = content_doc # --------------------------------------------------------------------- diff --git a/asl_rulebook2/webapp/globvars.py b/asl_rulebook2/webapp/globvars.py index ba807b5..68717ee 100644 --- a/asl_rulebook2/webapp/globvars.py +++ b/asl_rulebook2/webapp/globvars.py @@ -26,7 +26,7 @@ def on_request(): global _init_done if not _init_done or (request.path == "/" and request.args.get("reload")): try: - from asl_rulebook2.webapp.main import init_webapp + from asl_rulebook2.webapp.startup import init_webapp init_webapp() finally: # NOTE: It's important to set this, even if initialization failed, so we don't diff --git a/asl_rulebook2/webapp/main.py b/asl_rulebook2/webapp/main.py index 85da6b3..e4cf967 100644 --- a/asl_rulebook2/webapp/main.py +++ b/asl_rulebook2/webapp/main.py @@ -8,25 +8,10 @@ import logging from flask import render_template, jsonify, abort from asl_rulebook2.webapp import app, globvars, shutdown_event -from asl_rulebook2.webapp.content import load_content_docs -from asl_rulebook2.webapp.search import init_search from asl_rulebook2.webapp.utils import parse_int # --------------------------------------------------------------------- -def init_webapp(): - """Initialize the webapp. - - IMPORTANT: This is called on the first Flask request, but can also be called multiple times - after that by the test suite, to reset the webapp before each test. - """ - # initialize the webapp - logger = logging.getLogger( "startup" ) - load_content_docs( logger ) - init_search( logger ) - -# --------------------------------------------------------------------- - @app.route( "/" ) def main(): """Return the main page.""" diff --git a/asl_rulebook2/webapp/search.py b/asl_rulebook2/webapp/search.py index 2d88c78..86154ae 100644 --- a/asl_rulebook2/webapp/search.py +++ b/asl_rulebook2/webapp/search.py @@ -321,7 +321,7 @@ def _adjust_sort_order( results ): # --------------------------------------------------------------------- -def init_search( logger ): +def init_search( startup_msgs, logger ): """Initialize the search engine.""" # initialize @@ -376,11 +376,11 @@ def init_search( logger ): assert len(_fts_index_entries) == _get_row_count( conn, "searchable" ) # load the search config - load_search_config( logger ) + load_search_config( startup_msgs, logger ) # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -def load_search_config( logger ): +def load_search_config( startup_msgs, logger ): """Load the search config.""" # initialize @@ -411,12 +411,16 @@ def load_search_config( logger ): _SEARCH_TERM_ADJUSTMENTS[ key ].update( vals ) # load the search replacements - def load_search_replacements( fname ): + def load_search_replacements( fname, ftype ): if not os.path.isfile( fname ): return logger.info( "Loading search replacements: %s", fname ) - with open( fname, "r", encoding="utf-8" ) as fp: - data = json.load( fp ) + try: + with open( fname, "r", encoding="utf-8" ) as fp: + data = json.load( fp ) + except Exception as ex: #pylint: disable=broad-except + startup_msgs.warning( "Can't load {} search replacements.".format( ftype ), str(ex) ) + return nitems = 0 for key, val in data.items(): if key.startswith( "_" ): @@ -425,16 +429,20 @@ def load_search_config( logger ): add_search_term_adjustment( key, val ) nitems += 1 logger.info( "- Loaded %s.", plural(nitems,"search replacement","search replacements") ) - load_search_replacements( make_config_path( "search-replacements.json" ) ) - load_search_replacements( make_data_path( "search-replacements.json" ) ) + load_search_replacements( make_config_path( "search-replacements.json" ), "default" ) + load_search_replacements( make_data_path( "search-replacements.json" ), "user" ) # load the search aliases - def load_search_aliases( fname ): + def load_search_aliases( fname, ftype ): if not os.path.isfile( fname ): return logger.info( "Loading search aliases: %s", fname ) - with open( fname, "r", encoding="utf-8" ) as fp: - data = json.load( fp ) + try: + with open( fname, "r", encoding="utf-8" ) as fp: + data = json.load( fp ) + except Exception as ex: #pylint: disable=broad-except + startup_msgs.warning( "Can't load {} search aliases.".format( ftype ), str(ex) ) + return nitems = 0 for keys, aliases in data.items(): if keys.startswith( "_" ): @@ -444,16 +452,20 @@ def load_search_config( logger ): add_search_term_adjustment( key, set( itertools.chain( aliases, [key] ) ) ) nitems += 1 logger.info( "- Loaded %s.", plural(nitems,"search aliases","search aliases") ) - load_search_aliases( make_config_path( "search-aliases.json" ) ) - load_search_aliases( make_data_path( "search-aliases.json" ) ) + load_search_aliases( make_config_path( "search-aliases.json" ), "default" ) + load_search_aliases( make_data_path( "search-aliases.json" ), "user" ) # load the search synonyms - def load_search_synonyms( fname ): + def load_search_synonyms( fname, ftype ): if not os.path.isfile( fname ): return logger.info( "Loading search synonyms: %s", fname ) - with open( fname, "r", encoding="utf-8" ) as fp: - data = json.load( fp ) + try: + with open( fname, "r", encoding="utf-8" ) as fp: + data = json.load( fp ) + except Exception as ex: #pylint: disable=broad-except + startup_msgs.warning( "Can't load {} search synonyms.".format( ftype ), str(ex) ) + return nitems = 0 for synonyms in data: if isinstance( synonyms, str ): @@ -464,8 +476,8 @@ def load_search_config( logger ): add_search_term_adjustment( term, synonyms ) nitems += 1 logger.info( "- Loaded %s.", plural(nitems,"search synonym","search synonyms") ) - load_search_synonyms( make_config_path( "search-synonyms.json" ) ) - load_search_synonyms( make_data_path( "search-synonyms.json" ) ) + load_search_synonyms( make_config_path( "search-synonyms.json" ), "default" ) + load_search_synonyms( make_data_path( "search-synonyms.json" ), "user" ) # --------------------------------------------------------------------- diff --git a/asl_rulebook2/webapp/startup.py b/asl_rulebook2/webapp/startup.py new file mode 100644 index 0000000..cf73ae6 --- /dev/null +++ b/asl_rulebook2/webapp/startup.py @@ -0,0 +1,62 @@ +""" Manage the startup process. """ + +import logging +from collections import defaultdict + +from flask import jsonify + +from asl_rulebook2.webapp import app +from asl_rulebook2.webapp.content import load_content_docs +from asl_rulebook2.webapp.search import init_search + +_logger = logging.getLogger( "startup" ) +_startup_msgs = None + +# --------------------------------------------------------------------- + +def init_webapp(): + """Initialize the webapp. + + IMPORTANT: This is called on the first Flask request, but can also be called multiple times + after that by the test suite, to reset the webapp before each test. + """ + + # initialize + global _startup_msgs + _startup_msgs = StartupMsgs() + + # initialize the webapp + load_content_docs( _startup_msgs, _logger ) + init_search( _startup_msgs, _logger ) + +# --------------------------------------------------------------------- + +@app.route( "/startup-msgs" ) +def get_startup_msgs(): + """Return any messages issued during startup.""" + return jsonify( _startup_msgs.msgs ) + +# --------------------------------------------------------------------- + +class StartupMsgs: + """Store messages issued during startup.""" + + def __init__( self ): + self.msgs = defaultdict( list ) + + #pylint: disable=missing-function-docstring + def info( self, msg, msg_info=None ): + return self._add_msg( "info", msg, msg_info ) + def warning( self, msg, msg_info=None ): + return self._add_msg( "warning", msg, msg_info ) + def error( self, msg, msg_info=None ): + return self._add_msg( "error", msg, msg_info ) + + def _add_msg( self, msg_type, msg, msg_info ): + """Add a startup message.""" + if msg_info: + self.msgs[ msg_type ].append( ( msg, msg_info ) ) + getattr( _logger, msg_type )( "%s\n %s", msg, msg_info ) + else: + self.msgs[ msg_type ].append( msg ) + getattr( _logger, msg_type )( "%s", msg ) diff --git a/asl_rulebook2/webapp/static/MainApp.js b/asl_rulebook2/webapp/static/MainApp.js index 3619d18..f0f413b 100644 --- a/asl_rulebook2/webapp/static/MainApp.js +++ b/asl_rulebook2/webapp/static/MainApp.js @@ -1,4 +1,4 @@ -import { showErrorMsg } from "./utils.js" ; +import { showErrorMsg, showNotificationMsg } from "./utils.js" ; // parse any URL parameters export let gUrlParams = new URLSearchParams( window.location.search.substring(1) ) ; @@ -45,6 +45,7 @@ gMainApp.component( "main-app", { this.getContentDocs( this ), ] ).then( () => { this.isLoaded = true ; + this.showStartupMsgs() ; $( "#query-string" ).focus() ; // nb: because autofocus on the doesn't work :-/ } ) ; }, @@ -73,6 +74,23 @@ gMainApp.component( "main-app", { } ) ; }, + showStartupMsgs() { + $.getJSON( gGetStartupMsgsUrl, (resp) => { //eslint-disable-line no-undef + // show any startup messages + [ "info", "warning", "error" ].forEach( (msgType) => { + if ( ! resp[msgType] ) + return ; + resp[msgType].forEach( (msg) => { + if ( Array.isArray( msg ) ) + msg = msg[0] + "
" + msg[1] + "
" ; + showNotificationMsg( msgType, msg ) ; + } ) ; + } ) ; + } ).fail( (xhr, status, errorMsg) => { //eslint-disable-line no-unused-vars + showErrorMsg( "Couldn't get the startup messages." ) ; + } ) ; + }, + }, } ) ; diff --git a/asl_rulebook2/webapp/static/css/global.css b/asl_rulebook2/webapp/static/css/global.css index 9815bc8..8648c77 100644 --- a/asl_rulebook2/webapp/static/css/global.css +++ b/asl_rulebook2/webapp/static/css/global.css @@ -12,4 +12,4 @@ .growl .growl-close { position: absolute ; top: 0 ; right: 6px ; } .growl .growl-title { display: none ; } .growl .pre { font-family: monospace ; } -.growl div.pre { margin: 0 0 15px 15px ; font-size: 80% ; } +.growl div.pre { margin: 0 0 10px 10px ; font-size: 80% ; } diff --git a/asl_rulebook2/webapp/static/utils.js b/asl_rulebook2/webapp/static/utils.js index cb7a9ea..31ce5e9 100644 --- a/asl_rulebook2/webapp/static/utils.js +++ b/asl_rulebook2/webapp/static/utils.js @@ -1,3 +1,5 @@ +import { gUrlParams } from "./MainApp.js" ; + // -------------------------------------------------------------------- const _HILITE_REGEXES = [ @@ -16,15 +18,21 @@ export function fixupSearchHilites( val ) // -------------------------------------------------------------------- -export function showInfoMsg( msg ) { _doShowNotificationMsg( "notice", msg ) ; } -export function showWarningMsg( msg ) { _doShowNotificationMsg( "warning", msg ) ; } -export function showErrorMsg( msg ) { _doShowNotificationMsg( "error", msg ) ; } +export function showInfoMsg( msg ) { showNotificationMsg( "notice", msg ) ; } +export function showWarningMsg( msg ) { showNotificationMsg( "warning", msg ) ; } +export function showErrorMsg( msg ) { showNotificationMsg( "error", msg ) ; } -function _doShowNotificationMsg( msgType, msg ) +export function showNotificationMsg( msgType, msg ) { + if ( gUrlParams.get( "store-msgs" ) ) { + // store the message for the test suite + $( "#_last-" + msgType + "-msg_" ).val( msg ) ; + return ; + } + // show the notification message $.growl( { - style: msgType, + style: (msgType == "info") ? "notice" : msgType, title: null, message: msg, location: "br", diff --git a/asl_rulebook2/webapp/templates/index.html b/asl_rulebook2/webapp/templates/index.html index 4ec8181..8d0ae32 100644 --- a/asl_rulebook2/webapp/templates/index.html +++ b/asl_rulebook2/webapp/templates/index.html @@ -20,6 +20,8 @@
+ {# NOTE: We include some elements to support the test suite, and since they can be updated by the test suite, we manage them manually (instead of using Vue). #} + {%include "testing.html"%} {%if WEB_DEBUG%} @@ -42,7 +44,8 @@ diff --git a/asl_rulebook2/webapp/templates/testing.html b/asl_rulebook2/webapp/templates/testing.html new file mode 100644 index 0000000..98975a0 --- /dev/null +++ b/asl_rulebook2/webapp/templates/testing.html @@ -0,0 +1,4 @@ + + + + diff --git a/asl_rulebook2/webapp/tests/fixtures/invalid-footnotes/test.footnotes b/asl_rulebook2/webapp/tests/fixtures/invalid-footnotes/test.footnotes new file mode 100644 index 0000000..c0b7d41 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/invalid-footnotes/test.footnotes @@ -0,0 +1 @@ +Invalid JSON. diff --git a/asl_rulebook2/webapp/tests/fixtures/invalid-footnotes/test.index b/asl_rulebook2/webapp/tests/fixtures/invalid-footnotes/test.index new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/invalid-footnotes/test.index @@ -0,0 +1 @@ +{} diff --git a/asl_rulebook2/webapp/tests/fixtures/invalid-index/test.index b/asl_rulebook2/webapp/tests/fixtures/invalid-index/test.index new file mode 100644 index 0000000..c0b7d41 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/invalid-index/test.index @@ -0,0 +1 @@ +Invalid JSON. diff --git a/asl_rulebook2/webapp/tests/fixtures/invalid-search-aliases/search-aliases.json b/asl_rulebook2/webapp/tests/fixtures/invalid-search-aliases/search-aliases.json new file mode 100644 index 0000000..c0b7d41 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/invalid-search-aliases/search-aliases.json @@ -0,0 +1 @@ +Invalid JSON. diff --git a/asl_rulebook2/webapp/tests/fixtures/invalid-search-replacements/search-replacements.json b/asl_rulebook2/webapp/tests/fixtures/invalid-search-replacements/search-replacements.json new file mode 100644 index 0000000..c0b7d41 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/invalid-search-replacements/search-replacements.json @@ -0,0 +1 @@ +Invalid JSON. diff --git a/asl_rulebook2/webapp/tests/fixtures/invalid-search-synonyms/search-synonyms.json b/asl_rulebook2/webapp/tests/fixtures/invalid-search-synonyms/search-synonyms.json new file mode 100644 index 0000000..c0b7d41 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/invalid-search-synonyms/search-synonyms.json @@ -0,0 +1 @@ +Invalid JSON. diff --git a/asl_rulebook2/webapp/tests/fixtures/invalid-targets/test.index b/asl_rulebook2/webapp/tests/fixtures/invalid-targets/test.index new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/invalid-targets/test.index @@ -0,0 +1 @@ +{} diff --git a/asl_rulebook2/webapp/tests/fixtures/invalid-targets/test.targets b/asl_rulebook2/webapp/tests/fixtures/invalid-targets/test.targets new file mode 100644 index 0000000..c0b7d41 --- /dev/null +++ b/asl_rulebook2/webapp/tests/fixtures/invalid-targets/test.targets @@ -0,0 +1 @@ +Invalid JSON. diff --git a/asl_rulebook2/webapp/tests/test_search.py b/asl_rulebook2/webapp/tests/test_search.py index 154a705..0123d0e 100644 --- a/asl_rulebook2/webapp/tests/test_search.py +++ b/asl_rulebook2/webapp/tests/test_search.py @@ -7,6 +7,7 @@ from selenium.webdriver.common.keys import Keys from asl_rulebook2.utils import strip_html from asl_rulebook2.webapp.search import load_search_config, _make_fts_query_string +from asl_rulebook2.webapp.startup import StartupMsgs from asl_rulebook2.webapp.tests.utils import init_webapp, select_tabbed_page, get_classes, \ wait_for, find_child, find_children @@ -124,11 +125,13 @@ def test_make_fts_query_string(): """Test generating the FTS query string.""" # initialize - load_search_config( logging.getLogger("_unknown_") ) + startup_msgs = StartupMsgs() + load_search_config( startup_msgs, logging.getLogger("_unknown_") ) def check( query, expected ): fts_query_string, _ = _make_fts_query_string(query) assert fts_query_string == expected + assert not startup_msgs.msgs # test some query strings check( "", "" ) diff --git a/asl_rulebook2/webapp/tests/test_startup.py b/asl_rulebook2/webapp/tests/test_startup.py new file mode 100644 index 0000000..97c86e2 --- /dev/null +++ b/asl_rulebook2/webapp/tests/test_startup.py @@ -0,0 +1,55 @@ +""" Test the startup process. """ + +from asl_rulebook2.webapp.tests.utils import init_webapp, \ + wait_for_warning_msg, wait_for_error_msg, find_children + +# --------------------------------------------------------------------- + +def test_load_content_docs( webapp, webdriver ): + """Test loading content docs.""" + + # test handling of an invalid data directory + webapp.control_tests.set_data_dir( "_unknown_" ) + init_webapp( webapp, webdriver ) + wait_for_error_msg( 2, "Invalid data directory.", contains=True ) + + # test handling of an invalid index file + webapp.control_tests.set_data_dir( "invalid-index" ) + init_webapp( webapp, webdriver ) + wait_for_error_msg( 2, "Couldn't load \"test.index\".", contains=True ) + # NOTE: If we can't load the index file, the content doc is useless and we don't load it at all. + # If any of the associated files are invalid, the content doc is loaded (i.e. a tab will be shown + # for it), and we degrade gracefully. + assert len( find_children( "#content .tabbed-page" ) ) == 0 + + # test handling of an invalid targets file + webapp.control_tests.set_data_dir( "invalid-targets" ) + init_webapp( webapp, webdriver ) + wait_for_warning_msg( 2, "Couldn't load \"test.targets\".", contains=True ) + assert len( find_children( "#content .tabbed-page" ) ) == 1 + + # test handling of an invalid footnotes file + webapp.control_tests.set_data_dir( "invalid-footnotes" ) + init_webapp( webapp, webdriver ) + wait_for_warning_msg( 2, "Couldn't load \"test.footnotes\".", contains=True ) + assert len( find_children( "#content .tabbed-page" ) ) == 1 + +# --------------------------------------------------------------------- + +def test_init_search( webapp, webdriver ): + """Test initializing the search engine.""" + + # test handling of an invalid search replacements file + webapp.control_tests.set_data_dir( "invalid-search-replacements" ) + init_webapp( webapp, webdriver ) + wait_for_warning_msg( 2, "Can't load user search replacements.", contains=True ) + + # test handling of an invalid search aliases file + webapp.control_tests.set_data_dir( "invalid-search-aliases" ) + init_webapp( webapp, webdriver ) + wait_for_warning_msg( 2, "Can't load user search aliases.", contains=True ) + + # test handling of an invalid search synonyms file + webapp.control_tests.set_data_dir( "invalid-search-synonyms" ) + init_webapp( webapp, webdriver ) + wait_for_warning_msg( 2, "Can't load user search synonyms.", contains=True ) diff --git a/asl_rulebook2/webapp/tests/utils.py b/asl_rulebook2/webapp/tests/utils.py index 91d09b2..22182a3 100644 --- a/asl_rulebook2/webapp/tests/utils.py +++ b/asl_rulebook2/webapp/tests/utils.py @@ -1,7 +1,10 @@ """ Helper utilities. """ +import sys +import uuid + from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import NoSuchElementException, TimeoutException from asl_rulebook2.webapp import tests as webapp_tests @@ -27,6 +30,7 @@ def init_webapp( webapp, webdriver, **options ): # FUDGE! Headless Chrome doesn't want to show the PDF in the browser, # it downloads the file and saves it in the current directory :wtf: options["no-content"] = 1 + options["store-msgs"] = 1 # nb: so that we can retrive notification messages options["reload"] = 1 # nb: force the webapp to reload webdriver.get( webapp.url_for( "main", **options ) ) _wait_for_webapp() @@ -70,6 +74,32 @@ def _get_tab_ids( sel ): # --------------------------------------------------------------------- +#pylint: disable=multiple-statements,missing-function-docstring +def get_last_info(): return get_stored_msg( "info" ) +def get_last_warning_msg(): return get_stored_msg( "warning" ) +def get_last_error_msg(): return get_stored_msg( "error" ) +#pylint: enable=multiple-statements,missing-function-docstring + +def get_stored_msg( msg_type ): + """Get a message stored for us by the front-end.""" + elem = find_child( "#_last-{}-msg_".format(msg_type), _webdriver ) + assert elem.tag_name == "textarea" + return elem.get_attribute( "value" ) + +def set_stored_msg( msg_type, val ): + """Set a message for the front-end.""" + elem = find_child( "#_last-{}-msg_".format(msg_type), _webdriver ) + assert elem.tag_name == "textarea" + _webdriver.execute_script( "arguments[0].value = arguments[1]", elem, val ) + +def set_stored_msg_marker( msg_type ): + """Store marker text in the message buffer (so we can tell if the front-end changes it).""" + marker = "marker:{}:{}".format( msg_type, uuid.uuid4() ) + set_stored_msg( msg_type, marker ) + return marker + +# --------------------------------------------------------------------- + def find_child( sel, parent=None ): """Find a single child element.""" try: @@ -101,6 +131,29 @@ def wait_for( timeout, func ): lambda driver: func() ) +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#pylint: disable=missing-function-docstring +def wait_for_info_msg( timeout, expected, contains=True ): + return _do_wait_for_msg( timeout, "info", expected, contains ) +def wait_for_warning_msg( timeout, expected, contains=True ): + return _do_wait_for_msg( timeout, "warning", expected, contains ) +def wait_for_error_msg( timeout, expected, contains=True ): + return _do_wait_for_msg( timeout, "error", expected, contains ) +#pylint: enable=missing-function-docstring + +def _do_wait_for_msg( timeout, msg_type, expected, contains ): + """Wait for a message to be issued.""" + func = getattr( sys.modules[__name__], "get_last_{}_msg".format( msg_type ) ) + try: + wait_for( timeout, + lambda: expected in func() if contains else expected == func() + ) + except TimeoutException: + print( "ERROR: Didn't get expected {} message: {}".format( msg_type, expected ) ) + print( "- last {} message: {}".format( msg_type, func() ) ) + assert False + # --------------------------------------------------------------------- def get_pytest_option( opt ):