Fixed some timing issues in the scenario search tests.

master
Pacman Ghost 4 years ago
parent b318507953
commit 6fa454dd9a
  1. 8
      vasl_templates/webapp/static/scenarios.js
  2. 205
      vasl_templates/webapp/tests/test_scenario_search.py
  3. 4
      vasl_templates/webapp/tests/utils.py

@ -395,6 +395,13 @@ function loadScenarioCard( $target, scenarioId, briefMode, scenarioDateOverride,
} }
gActiveScenaridCardRequest = scenarioId ; gActiveScenaridCardRequest = scenarioId ;
// initialize
// NOTE: We tag the scenario card with a seq# for the benefit of the test suite, so that it can tell
// when a scenario card has finished loading, and it's safe to read values out of the UI.
// It won't work under load, but we only need it for the simple case of a single request coming in,
// and waiting for it to load completely.
var seqNo = $target.attr( "data-seqNo" ) || 1 ;
// load the specified scenario // load the specified scenario
var url = gGetScenarioCardUrl.replace( "ID", scenarioId ) ; var url = gGetScenarioCardUrl.replace( "ID", scenarioId ) ;
if ( briefMode ) if ( briefMode )
@ -431,6 +438,7 @@ function loadScenarioCard( $target, scenarioId, briefMode, scenarioDateOverride,
$target.html( $card ).fadeIn( 100 ) ; $target.html( $card ).fadeIn( 100 ) ;
$target.attr( "data-id", scenarioId ) ; $target.attr( "data-id", scenarioId ) ;
onDone( scenario ) ; onDone( scenario ) ;
$target.attr( "data-seqNo", parseInt(seqNo)+1 ) ;
} ) ; } ) ;
} ).fail( onError ) ; } ).fail( onError ) ;

@ -1,12 +1,11 @@
"""" Test scenario search. """ """" Test scenario search. """
import os import os
import time
import pytest import pytest
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import ElementClickInterceptedException, ElementNotInteractableException from selenium.common.exceptions import StaleElementReferenceException
from vasl_templates.webapp.tests.test_scenario_persistence import save_scenario, load_scenario from vasl_templates.webapp.tests.test_scenario_persistence import save_scenario, load_scenario
from vasl_templates.webapp.tests.utils import init_webapp, select_tab, new_scenario, \ from vasl_templates.webapp.tests.utils import init_webapp, select_tab, new_scenario, \
@ -76,37 +75,38 @@ def test_import_scenario( webapp, webdriver ):
# import the "full" scenario # import the "full" scenario
dlg = _do_scenario_search( "full", [1], webdriver ) dlg = _do_scenario_search( "full", [1], webdriver )
find_child( "button.import", dlg ).click() find_child( "button.import", dlg ).click()
_check_scenario( wait_for( 2, lambda: _check_scenario(
SCENARIO_NAME="Full content scenario", SCENARIO_ID="FCS-1", SCENARIO_NAME="Full content scenario", SCENARIO_ID="FCS-1",
SCENARIO_LOCATION="Some place", SCENARIO_LOCATION="Some place",
PLAYER_1="dutch", PLAYER_1_DESCRIPTION="1st Dutch Army", PLAYER_1="dutch", PLAYER_1_DESCRIPTION="1st Dutch Army",
PLAYER_2="romanian", PLAYER_2_DESCRIPTION="1st Romanian Army", PLAYER_2="romanian", PLAYER_2_DESCRIPTION="1st Romanian Army",
THEATER="PTO" THEATER="PTO"
) ) )
# import the "empty" scenario # import the "empty" scenario
_unlink_scenario() _unlink_scenario()
dlg = _do_scenario_search( "Untitled", ["no-content"], webdriver ) dlg = _do_scenario_search( "Untitled", ["no-content"], webdriver )
find_child( "button.import", dlg ).click() _import_scenario_and_confirm( dlg )
find_child( "button.confirm-import", dlg ).click()
# NOTE: Since there are no players defined in the scenario, what's on-screen will be left unchanged. # NOTE: Since there are no players defined in the scenario, what's on-screen will be left unchanged.
_check_scenario( wait_for( 2, lambda: _check_scenario(
SCENARIO_NAME="Untitled scenario (#no-content)", SCENARIO_ID="", SCENARIO_NAME="Untitled scenario (#no-content)", SCENARIO_ID="",
SCENARIO_LOCATION="", SCENARIO_LOCATION="",
PLAYER_1="dutch", PLAYER_1_DESCRIPTION="", PLAYER_1="dutch", PLAYER_1_DESCRIPTION="",
PLAYER_2="romanian", PLAYER_2_DESCRIPTION="", PLAYER_2="romanian", PLAYER_2_DESCRIPTION="",
THEATER="ETO" THEATER="ETO"
) ) )
def _check_scenario( **kwargs ): def _check_scenario( **kwargs ):
"""Check the scenario import.""" """Check the scenario import."""
keys = [ "SCENARIO_NAME", "SCENARIO_ID", "SCENARIO_LOCATION", "PLAYER_1_DESCRIPTION", "PLAYER_2_DESCRIPTION" ] for key in ["SCENARIO_NAME","SCENARIO_ID","SCENARIO_LOCATION","PLAYER_1_DESCRIPTION","PLAYER_2_DESCRIPTION"]:
for key in keys:
elem = find_child( "input[name='{}']".format( key ) ) elem = find_child( "input[name='{}']".format( key ) )
assert elem.get_attribute( "value" ) == kwargs[ key ] if elem.get_attribute( "value" ) != kwargs[ key ]:
assert get_player_nat( 1 ) == kwargs[ "PLAYER_1" ] return False
assert get_player_nat( 2 ) == kwargs[ "PLAYER_2" ] if get_player_nat( 1 ) != kwargs["PLAYER_1"] or get_player_nat( 2 ) != kwargs["PLAYER_2"]:
assert get_theater() == kwargs[ "THEATER" ] return False
if get_theater() != kwargs[ "THEATER" ]:
return False
return True
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -121,6 +121,10 @@ def test_import_warnings( webapp, webdriver ): #pylint: disable=too-many-stateme
find_child( "button.import", dlg ).click() find_child( "button.import", dlg ).click()
wait_for( 2, lambda: not dlg.is_displayed() ) wait_for( 2, lambda: not dlg.is_displayed() )
def check_warnings( expected ): #pylint: disable=missing-docstring
warnings = find_children( ".warnings input[type='checkbox']", dlg )
return [ w.get_attribute( "name" ) for w in warnings ] == expected
def do_test( param_name, expected_warning, expected_val, curr_val="CURR-VAL" ): #pylint: disable=missing-docstring def do_test( param_name, expected_warning, expected_val, curr_val="CURR-VAL" ): #pylint: disable=missing-docstring
# start with a new scenario # start with a new scenario
@ -137,22 +141,17 @@ def test_import_warnings( webapp, webdriver ): #pylint: disable=too-many-stateme
elem = find_child( "[name='{}']".format( param_name ) ) elem = find_child( "[name='{}']".format( param_name ) )
if expected_warning: if expected_warning:
# yup - make sure they are being shown # yup - make sure they are being shown
warnings = find_children( ".warnings input[type='checkbox']", dlg ) wait_for( 2, lambda: check_warnings( [expected_warning] ) )
if expected_warning:
assert [ w.get_attribute( "name" ) for w in warnings ] == [ expected_warning ]
else:
assert not warnings
# cancel the import # cancel the import
find_child( "button.cancel-import", dlg ).click() find_child( "button.cancel-import", dlg ).click()
wait_for( 2, lambda: not find_child( ".warnings", dlg ).is_displayed() ) wait_for( 2, lambda: not find_child( ".warnings", dlg ).is_displayed() )
# do the import again, and accept it # do the import again, and accept it
find_child( "#scenario-search button.import" ).click() _import_scenario_and_confirm( dlg )
find_child( "button.confirm-import", dlg ).click()
assert not dlg.is_displayed()
assert elem.get_attribute( "value" ) == expected_val assert elem.get_attribute( "value" ) == expected_val
else: else:
# nope - check that the import was done # nope - check that the import was done
assert not dlg.is_displayed() wait_for( 2, lambda: not dlg.is_displayed() )
# assert not dlg.is_displayed()
assert elem.get_attribute( "value" ) == expected_val assert elem.get_attribute( "value" ) == expected_val
# do the tests # do the tests
@ -171,10 +170,10 @@ def test_import_warnings( webapp, webdriver ): #pylint: disable=too-many-stateme
load_scenario( { load_scenario( {
"PLAYER_1": "dutch", "PLAYER_1": "dutch",
} ) } )
_do_scenario_search( "full", [1], webdriver ) dlg = _do_scenario_search( "full", [1], webdriver )
find_child( "#scenario-search button.import" ).click() find_child( "button.import", dlg ).click()
warnings = find_children( ".warnings input[type='checkbox']", dlg ) check_warnings( [] )
assert [ w.get_attribute( "name" ) for w in warnings ] == [] wait_for( 2, lambda: not dlg.is_displayed() )
# test importing a scenario on top of existing OB owned by the same nationality # test importing a scenario on top of existing OB owned by the same nationality
new_scenario() new_scenario()
@ -182,8 +181,8 @@ def test_import_warnings( webapp, webdriver ): #pylint: disable=too-many-stateme
"PLAYER_1": "dutch", "PLAYER_1": "dutch",
"OB_SETUPS_1": [ { "caption": "Dutch setup note" } ] "OB_SETUPS_1": [ { "caption": "Dutch setup note" } ]
} ) } )
_do_scenario_search( "full", [1], webdriver ) dlg = _do_scenario_search( "full", [1], webdriver )
find_child( "#scenario-search button.import" ).click() find_child( "button.import", dlg ).click()
wait_for( 2, lambda: not dlg.is_displayed() ) wait_for( 2, lambda: not dlg.is_displayed() )
# test importing a scenario on top of existing OB owned by the different nationality # test importing a scenario on top of existing OB owned by the different nationality
@ -192,18 +191,13 @@ def test_import_warnings( webapp, webdriver ): #pylint: disable=too-many-stateme
"PLAYER_1": "german", "PLAYER_1": "german",
"OB_SETUPS_1": [ { "caption": "German setup note" } ] "OB_SETUPS_1": [ { "caption": "German setup note" } ]
} ) } )
_do_scenario_search( "full", [1], webdriver ) dlg = _do_scenario_search( "full", [1], webdriver )
find_child( "#scenario-search button.import", dlg ).click() find_child( "button.import", dlg ).click()
warnings = wait_for( 2, lambda: find_children( ".warnings input[type='checkbox']", dlg ) ) warnings = wait_for( 2, lambda: find_children( ".warnings input[type='checkbox']", dlg ) )
assert [ w.get_attribute( "name" ) for w in warnings ] == [ "defender_name" ] assert [ w.get_attribute( "name" ) for w in warnings ] == [ "defender_name" ]
assert not warnings[0].is_selected() assert not warnings[0].is_selected()
try: wait_for( 2, lambda: warnings[0].is_displayed() )
warnings[0].click() warnings[0].click()
except (ElementClickInterceptedException, ElementNotInteractableException):
# FUDGE! We sometimes get a "Other element would receive the click" (div.warning) error,
# I suspect because the warnings panel is still sliding up.
time.sleep( 0.5 )
warnings[0].click()
find_child( "button.confirm-import", dlg ).click() find_child( "button.confirm-import", dlg ).click()
assert not dlg.is_displayed() assert not dlg.is_displayed()
assert get_player_nat( 1 ) == "dutch" assert get_player_nat( 1 ) == "dutch"
@ -230,7 +224,7 @@ def test_oba_info( webapp, webdriver ):
check_oba_info( _unload_scenario_card(), expected ) check_oba_info( _unload_scenario_card(), expected )
# import the scenario and check the OBA info # import the scenario and check the OBA info
find_child( "button.import" ).click() find_child( "button.import", dlg ).click()
wait_for( 2, lambda: not dlg.is_displayed() ) wait_for( 2, lambda: not dlg.is_displayed() )
check_oba_info( _get_scenario_info(), expected ) check_oba_info( _get_scenario_info(), expected )
@ -247,14 +241,14 @@ def test_oba_info( webapp, webdriver ):
check_oba_info( _get_scenario_info(), expected ) check_oba_info( _get_scenario_info(), expected )
# check a scenario where only the defender has OBA # check a scenario where only the defender has OBA
dlg = _do_scenario_search( "Defender OBA", ["5b"], webdriver ) _do_scenario_search( "Defender OBA", ["5b"], webdriver )
check_oba_info( _unload_scenario_card(), [ check_oba_info( _unload_scenario_card(), [
[ "Burmese", "-", "-" ], [ "Burmese", "-", "-" ],
None None
] ) ] )
# check a scenario where the attacker has OBA, the defender is an unknwon nationality # check a scenario where the attacker has OBA, the defender is an unknwon nationality
dlg = _do_scenario_search( "Attacker OBA", ["5c"], webdriver ) _do_scenario_search( "Attacker OBA", ["5c"], webdriver )
check_oba_info( _unload_scenario_card(), [ check_oba_info( _unload_scenario_card(), [
[ "The Other Guy", "?", "?" ], [ "The Other Guy", "?", "?" ],
[ "Russian", "3B", "4R" ] [ "Russian", "3B", "4R" ]
@ -410,70 +404,81 @@ def test_roar_linking( webapp, webdriver ):
def check( bgraph, connect, disconnect ): def check( bgraph, connect, disconnect ):
"""Check the scenario card.""" """Check the scenario card."""
# unload the scenario card
if find_child( "#scenario-search" ).is_displayed():
parent = find_child( "#scenario-search .scenario-card" )
elif find_child( "#scenario-info-dialog" ).is_displayed():
parent = find_child( "#scenario-info-dialog .scenario-card" )
else:
assert False
card = _unload_scenario_card()
# check if the balance graph is shown # check if the balance graph is shown
try:
card = _unload_scenario_card()
except StaleElementReferenceException:
# NOTE: We can get here if the scenario card is reloaded while we're reading it.
return False
if bgraph: if bgraph:
balance = card["balances"]["roar"] balance = card["balances"].get( "roar" )
assert balance[0]["name"] == bgraph[0] if not balance:
assert balance[1]["name"] == bgraph[1] return False
if balance[0]["name"] != bgraph[0] or balance[1]["name"] != bgraph[1]:
return False
else: else:
assert "roar" not in card["balances"] if "roar" in card["balances"]:
return False
# check if the "connect to ROAR" button is shown # check if the "connect to ROAR" button is shown
elem = find_child( ".connect-roar", parent ) elem = find_child( "#scenario-info-dialog .connect-roar" )
if connect: if connect:
assert elem.is_displayed() if not elem.is_displayed():
return False
else: else:
assert not elem.is_displayed() if elem and elem.is_displayed():
return False
# check if the "disconnect from ROAR" is shown # check if the "disconnect from ROAR" is shown
elem = find_child( ".disconnect-roar", parent ) elem = find_child( "#scenario-info-dialog .disconnect-roar" )
if disconnect: if disconnect:
assert elem.is_displayed() if not elem.is_displayed():
return False
else: else:
assert not elem or not elem.is_displayed() if elem and elem.is_displayed():
return False
return True
def disconnect_roar():
"""Disconnect the scenario from ROAR."""
btn = find_child( "#scenario-info-dialog .disconnect-roar" )
btn.click()
wait_for( 2, lambda: not btn.is_displayed() )
wait_for( 2, lambda: check( None, True, False ) )
# import the "Fighting Withdrawal" scenario # import the "Fighting Withdrawal" scenario
_do_scenario_search( "withdrawal", [2], webdriver ) dlg = _do_scenario_search( "withdrawal", [2], webdriver )
check( ["Russian","Finnish"], False, False ) wait_for( 2, lambda: check( ["Russian","Finnish"], False, False ) )
find_child( "#scenario-search button.import" ).click() find_child( "#scenario-search button.import" ).click()
wait_for( 2, lambda: not dlg.is_displayed() )
# connect to another ROAR scenario # connect to another ROAR scenario
find_child( "button.scenario-search" ).click() find_child( "button.scenario-search" ).click()
check( ["Russian","Finnish"], False, True ) wait_for( 2, lambda: check( ["Russian","Finnish"], False, True ) )
find_child( "#scenario-info-dialog .disconnect-roar" ).click() disconnect_roar()
check( None, True, False )
find_child( "#scenario-info-dialog .connect-roar" ).click() find_child( "#scenario-info-dialog .connect-roar" ).click()
dlg = wait_for_elem( 2, ".ui-dialog.select-roar-scenario" ) dlg = wait_for_elem( 2, ".ui-dialog.select-roar-scenario" )
find_child( ".select2-search__field", dlg ).send_keys( "another" ) find_child( ".select2-search__field", dlg ).send_keys( "another" )
find_child( ".select2-search__field", dlg ).send_keys( Keys.RETURN ) find_child( ".select2-search__field", dlg ).send_keys( Keys.RETURN )
check( ["British","French"], False, True ) wait_for( 2, lambda: check( ["British","French"], False, True ) )
find_child( ".ui-dialog.scenario-info button.ok" ).click() find_child( ".ui-dialog.scenario-info button.ok" ).click()
# disconnect from the ROAR scenario # disconnect from the ROAR scenario
find_child( "button.scenario-search" ).click() find_child( "button.scenario-search" ).click()
check( ["British","French"], False, True ) wait_for( 2, lambda: check( ["British","French"], False, True ) )
find_child( "#scenario-info-dialog .disconnect-roar" ).click() disconnect_roar()
check( None, True, False )
find_child( ".ui-dialog.scenario-info button.ok" ).click() find_child( ".ui-dialog.scenario-info button.ok" ).click()
# connect to a ROAR scenario # connect to a ROAR scenario
find_child( "button.scenario-search" ).click() find_child( "button.scenario-search" ).click()
check( None, True, False ) wait_for( 2, lambda: check( None, True, False ) )
find_child( "#scenario-info-dialog .connect-roar" ).click() find_child( "#scenario-info-dialog .connect-roar" ).click()
dlg = wait_for_elem( 2, ".ui-dialog.select-roar-scenario" ) dlg = wait_for_elem( 2, ".ui-dialog.select-roar-scenario" )
elem = find_child( ".select2-search__field", dlg ) elem = find_child( ".select2-search__field", dlg )
elem.send_keys( "withdrawal" ) elem.send_keys( "withdrawal" )
elem.send_keys( Keys.RETURN ) elem.send_keys( Keys.RETURN )
check( ["Russian","Finnish"], False, True ) wait_for( 2, lambda: check( ["Russian","Finnish"], False, True ) )
find_child( ".ui-dialog.scenario-info button.ok" ).click() find_child( ".ui-dialog.scenario-info button.ok" ).click()
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
@ -512,15 +517,13 @@ def test_scenario_linking( webapp, webdriver ):
check( "1" ) check( "1" )
# import the "empty" scenario (on top of the current scenario) # import the "empty" scenario (on top of the current scenario)
_do_scenario_search( "Untitled", ["no-content"], webdriver ) dlg = _do_scenario_search( "Untitled", ["no-content"], webdriver )
find_child( "#scenario-search button.import" ).click() _import_scenario_and_confirm( dlg )
find_child( "#scenario-search button.confirm-import" ).click()
check( "no-content" ) check( "no-content" )
# import the "Fighting Withdrawal" scenario (on top of the current scenario) # import the "Fighting Withdrawal" scenario (on top of the current scenario)
_do_scenario_search( "Fighting Withdrawal", [2], webdriver ) dlg = _do_scenario_search( "Fighting Withdrawal", [2], webdriver )
find_child( "#scenario-search button.import" ).click() _import_scenario_and_confirm( dlg )
find_child( "#scenario-search button.confirm-import" ).click()
check( "2" ) check( "2" )
# unlink the scenario # unlink the scenario
@ -541,12 +544,29 @@ def _do_scenario_search( query, expected, webdriver ):
dlg = wait_for_elem( 2, "#scenario-search" ) dlg = wait_for_elem( 2, "#scenario-search" )
ActionChains( webdriver ).key_up( Keys.SHIFT ).perform() ActionChains( webdriver ).key_up( Keys.SHIFT ).perform()
# initialize
card = find_child( ".scenario-card", dlg )
seq_no = card.get_attribute( "data-seqNo" )
# do the search and check the results # do the search and check the results
elem = find_child( "input.select2-search__field", dlg ) elem = find_child( "input.select2-search__field", dlg )
elem.clear() elem.clear()
elem.send_keys( query ) # IMPORTANT: We can't use send_keys() here because it simulates a key-press for each character in the query,
results = _unload_search_results() # and the incremental search feature means that we will constantly be loading scenario cards as the results
assert [ r[0] for r in results ] == [ str(e) for e in expected ] # change, which makes it difficult for us to be able to tell when everything's stopped and it's safe to unload
# the scenario card. Instead, we manually load the text box and trigger an event to update the UI.
webdriver.execute_script(
"arguments[0].value = arguments[1] ; $(arguments[0]).trigger('input')",
elem, query
)
def check_search_results(): #pylint: disable=missing-docstring
results = _unload_search_results()
return [ r[0] for r in results ] == [ str(e) for e in expected ]
wait_for( 2, check_search_results )
# wait for the scenario card to finish loading
# NOTE: We do this here since the typical use case is to search for something, then check what was found.
wait_for( 2, lambda: card.get_attribute( "data-seqNo" ) != seq_no )
return dlg return dlg
@ -560,12 +580,8 @@ def _unload_search_results():
def _unload_scenario_card(): #pylint: disable=too-many-branches,too-many-locals def _unload_scenario_card(): #pylint: disable=too-many-branches,too-many-locals
"""Unload the scenario card.""" """Unload the scenario card."""
if find_child( "#scenario-search" ).is_displayed(): # initialize
card = find_child( "#scenario-search .scenario-card" ) card = wait_for( 2, _find_scenario_card )
elif find_child( "#scenario-info-dialog" ).is_displayed():
card = find_child( "#scenario-info-dialog .scenario-card" )
else:
assert False
results = {} results = {}
# unload the basic text content # unload the basic text content
@ -653,7 +669,7 @@ def _unload_scenario_card(): #pylint: disable=too-many-branches,too-many-locals
comments = find_child( ".{} .comments".format(player), oba ).text comments = find_child( ".{} .comments".format(player), oba ).text
if comments: if comments:
oba_info[-1].extend( comments.split( "\n" ) ) oba_info[-1].extend( comments.split( "\n" ) )
elem = find_child( ".date-warning", oba ) elem = find_child( ".date-warning", oba )
if elem.is_displayed(): if elem.is_displayed():
oba_info.append( elem.text ) oba_info.append( elem.text )
results[ "oba" ] = oba_info results[ "oba" ] = oba_info
@ -754,3 +770,18 @@ def _unlink_scenario():
btn = find_children( ".ui-dialog .ui-dialog-buttonpane button" )[1] btn = find_children( ".ui-dialog .ui-dialog-buttonpane button" )[1]
assert btn.text == "Unlink" assert btn.text == "Unlink"
btn.click() btn.click()
def _import_scenario_and_confirm( dlg ):
"""Import a scenario, confirming any warnings."""
find_child( "button.import", dlg ).click()
btn = wait_for_elem( 2, "button.confirm-import", dlg )
btn.click()
wait_for( 2, lambda: not dlg.is_displayed() )
def _find_scenario_card():
"""Find the currently-displayed scenario card."""
if find_child( "#scenario-search" ).is_displayed():
return find_child( "#scenario-search .scenario-card" )
if find_child( "#scenario-info-dialog" ).is_displayed():
return find_child( "#scenario-info-dialog .scenario-card" )
return None

@ -589,11 +589,11 @@ def wait_for( timeout, func ):
assert time.time() - start_time < timeout assert time.time() - start_time < timeout
time.sleep( 0.1 ) time.sleep( 0.1 )
def wait_for_elem( timeout, elem_id, parent=None ): def wait_for_elem( timeout, sel, parent=None ):
"""Wait for an element to appear .""" """Wait for an element to appear ."""
args = { "elem": None } args = { "elem": None }
def check_elem(): #pylint: disable=missing-docstring def check_elem(): #pylint: disable=missing-docstring
args["elem"] = find_child( elem_id, parent ) args["elem"] = find_child( sel, parent )
return args["elem"] is not None and args["elem"].is_displayed() return args["elem"] is not None and args["elem"].is_displayed()
wait_for( timeout, check_elem ) wait_for( timeout, check_elem )
return args["elem"] return args["elem"]

Loading…
Cancel
Save