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 ):