|
|
|
""" Helper utilities. """
|
|
|
|
|
|
|
|
import os
|
|
|
|
import urllib.request
|
|
|
|
import json
|
|
|
|
import time
|
|
|
|
import re
|
|
|
|
import typing
|
|
|
|
import uuid
|
|
|
|
import logging
|
|
|
|
from collections import defaultdict
|
|
|
|
|
|
|
|
import lxml.html
|
|
|
|
from selenium.webdriver.support.ui import Select
|
|
|
|
from selenium.webdriver.common.keys import Keys
|
|
|
|
from selenium.webdriver.common.action_chains import ActionChains
|
|
|
|
from selenium.webdriver.common.by import By
|
|
|
|
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, WebDriverException
|
|
|
|
|
|
|
|
import vasl_templates.webapp.tests
|
|
|
|
|
|
|
|
# standard templates
|
|
|
|
_STD_TEMPLATES = {
|
|
|
|
"scenario": [
|
|
|
|
"scenario", "players", "victory_conditions", "scenario_notes", "ssr",
|
|
|
|
"nat_caps_1", # nb: "nat_caps_2" is functionally the same as this
|
|
|
|
"turn_track", # nb: this will only be used if a turn count has been specified
|
|
|
|
"compass",
|
|
|
|
],
|
|
|
|
"ob1": [ "ob_setup_1", "ob_note_1",
|
|
|
|
"ob_vehicles_1", "ob_vehicle_note_1", "ob_vehicles_ma_notes_1",
|
|
|
|
"ob_ordnance_1", "ob_ordnance_note_1", "ob_ordnance_ma_notes_1"
|
|
|
|
],
|
|
|
|
"ob2": [ "ob_setup_2", "ob_note_2",
|
|
|
|
"ob_vehicles_2", "ob_vehicle_note_2", "ob_vehicles_ma_notes_2",
|
|
|
|
"ob_ordnance_2", "ob_ordnance_note_2", "ob_ordnance_ma_notes_2"
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
# nationality-specific templates
|
|
|
|
_NAT_TEMPLATES = {
|
|
|
|
"german": [ "pf", "psk", "atmm" ],
|
|
|
|
"finnish": [ "pf-finnish" ],
|
|
|
|
"hungarian": [ "pf-hungarian" ],
|
|
|
|
"romanian": [ "pf-romanian", "atmm-romanian" ],
|
|
|
|
"russian": [ "mol", "mol-p" ],
|
|
|
|
"american": [ "baz" ],
|
|
|
|
"british": [ "piat" ],
|
|
|
|
"japanese": [ "thh" ],
|
|
|
|
"kfw-cpva": [ "baz-cpva16", "baz-cpva17" ],
|
|
|
|
}
|
|
|
|
|
|
|
|
_webapp = None
|
|
|
|
_webdriver = None
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
def init_webapp( webapp, webdriver, **options ):
|
|
|
|
"""Initialize the webapp."""
|
|
|
|
|
|
|
|
# initialize
|
|
|
|
global _webapp, _webdriver
|
|
|
|
_webapp = webapp
|
|
|
|
_webdriver = webdriver
|
|
|
|
|
|
|
|
# load the webapp
|
|
|
|
options[ "force-reinit" ] = 1 # nb: force the webapp to re-initialize
|
|
|
|
webdriver.get( webapp.url_for( "main", **options ) )
|
|
|
|
_wait_for_webapp()
|
|
|
|
|
|
|
|
# reset the user settings
|
|
|
|
webdriver.delete_all_cookies()
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def _wait_for_webapp():
|
|
|
|
"""Wait for the webapp to finish initialization."""
|
|
|
|
# FUDGE! The webapp sometimes takes a while to initialize (if we have a NIC active but
|
|
|
|
# no internet connectivity? Is Chrome looking for some update, or something like that?)
|
|
|
|
timeout = 30 if os.name == "nt" else 15
|
|
|
|
wait_for( timeout, lambda: find_child("#_page-loaded_") is not None )
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
def for_each_template( func ): #pylint: disable=too-many-branches
|
|
|
|
"""Test each template."""
|
|
|
|
|
|
|
|
# initialize
|
|
|
|
sel = Select( find_child( "#tabs-scenario select[name='TURN_TRACK_NTURNS']" ) )
|
|
|
|
has_turn_track = sel.first_selected_option.get_attribute( "value" ) != ""
|
|
|
|
|
|
|
|
# generate a list of all the templates we need to test
|
|
|
|
templates_to_test = set()
|
|
|
|
dname = os.path.join( os.path.split(__file__)[0], "../data/default-template-pack" )
|
|
|
|
for fname in os.listdir(dname):
|
|
|
|
fname,extn = os.path.splitext( fname )
|
|
|
|
if extn != ".j2":
|
|
|
|
continue
|
|
|
|
if fname == "turn_track" and not has_turn_track:
|
|
|
|
continue
|
|
|
|
if fname == "ob_vo":
|
|
|
|
templates_to_test.update( [ "ob_vehicles", "ob_ordnance" ] )
|
|
|
|
elif fname == "ob_vo_note":
|
|
|
|
templates_to_test.update( [ "ob_vehicle_note", "ob_ordnance_note" ] )
|
|
|
|
elif fname == "ob_ma_notes":
|
|
|
|
templates_to_test.update( [ "ob_vehicles_ma_notes", "ob_ordnance_ma_notes" ] )
|
|
|
|
else:
|
|
|
|
templates_to_test.add( fname )
|
|
|
|
|
|
|
|
# test the standard templates
|
|
|
|
for tab_id,template_ids in _STD_TEMPLATES.items():
|
|
|
|
for template_id in template_ids:
|
|
|
|
if template_id == "turn_track" and not has_turn_track:
|
|
|
|
continue
|
|
|
|
select_tab( tab_id )
|
|
|
|
orig_template_id = template_id
|
|
|
|
if template_id == "scenario_notes":
|
|
|
|
template_id = "scenario_note"
|
|
|
|
elif template_id.endswith( ("_1","_2") ):
|
|
|
|
template_id = template_id[:-2]
|
|
|
|
func( template_id, orig_template_id )
|
|
|
|
if orig_template_id not in ("ob_setup_2","ob_note_2",
|
|
|
|
"ob_vehicles_2","ob_vehicle_note_2","ob_vehicles_ma_notes_2",
|
|
|
|
"ob_ordnance_2","ob_ordnance_note_2","ob_ordnance_ma_notes_2"
|
|
|
|
):
|
|
|
|
templates_to_test.remove( template_id )
|
|
|
|
|
|
|
|
# test the nationality-specific templates
|
|
|
|
# NOTE: The buttons are the same on the OB1 and OB2 tabs, so we only test for player 1.
|
|
|
|
for nat,template_ids in _NAT_TEMPLATES.items():
|
|
|
|
set_player( 1, nat )
|
|
|
|
ask = find_child( "#ask" )
|
|
|
|
if ask and ask.is_displayed():
|
|
|
|
click_dialog_button( "OK" ) # nb: if the front-end is asking to confirm the player nationality change
|
|
|
|
select_tab( "ob1" )
|
|
|
|
for template_id in template_ids:
|
|
|
|
orig_template_id = template_id
|
|
|
|
if template_id.endswith( ("_1","_2") ):
|
|
|
|
template_id = template_id[:-2]
|
|
|
|
func( template_id, orig_template_id )
|
|
|
|
templates_to_test.remove( template_id )
|
|
|
|
|
|
|
|
# test the American BAZ 45 and BAZ 50
|
|
|
|
load_scenario_params( { "scenario": {
|
|
|
|
"PLAYER_1": "american",
|
|
|
|
"SCENARIO_THEATER": "Korea",
|
|
|
|
} } )
|
|
|
|
select_tab( "ob1" )
|
|
|
|
for template_id in ("baz45","baz50"):
|
|
|
|
func( template_id, template_id )
|
|
|
|
templates_to_test.remove( template_id )
|
|
|
|
|
|
|
|
# make sure we tested everything
|
|
|
|
assert not templates_to_test
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def select_tab( tab_id, webdriver=None ):
|
|
|
|
"""Select a tab in the main page."""
|
|
|
|
if not webdriver:
|
|
|
|
webdriver = _webdriver
|
|
|
|
elem = find_child( "#tabs .ui-tabs-nav a[href='#tabs-{}']".format( tab_id ), webdriver )
|
|
|
|
elem.click()
|
|
|
|
|
|
|
|
def select_tab_for_elem( elem ):
|
|
|
|
"""Select the tab that contains the specified element."""
|
|
|
|
select_tab( get_tab_for_elem( elem ) )
|
|
|
|
|
|
|
|
def get_tab_for_elem( elem ):
|
|
|
|
"""Identify the tab that contains the specified element."""
|
|
|
|
while elem.tag_name not in ("html","body"):
|
|
|
|
elem = elem.find_element( By.XPATH, ".." )
|
|
|
|
if elem.tag_name == "div":
|
|
|
|
div_id = elem.get_attribute( "id" )
|
|
|
|
if div_id.startswith( "tabs-" ):
|
|
|
|
return div_id[5:]
|
|
|
|
return None
|
|
|
|
|
|
|
|
def select_menu_option( menu_id, webdriver=None ):
|
|
|
|
"""Select a menu option."""
|
|
|
|
|
|
|
|
if not webdriver:
|
|
|
|
webdriver = _webdriver
|
|
|
|
elem = find_child( "#menu", webdriver )
|
|
|
|
elem.click()
|
|
|
|
elem = wait_for_elem( 2, "a.PopMenu-Link[data-name='{}']".format( menu_id ), webdriver )
|
|
|
|
elem.click()
|
|
|
|
wait_for( 2, lambda: find_child("#menu .PopMenu-Container",webdriver) is None ) # nb: wait for the menu to go away
|
|
|
|
|
|
|
|
# FUDGE! The delay above is not enough, I suspect because Selenium is deciding that the PopMenu container
|
|
|
|
# is hidden if it has a very low opacity, but it's still blocking any clicks we want to do after we return.
|
|
|
|
# We work around this by trying to click on a dummy button, until it works :-/
|
|
|
|
class PopMenuHack:
|
|
|
|
"""Enable/disable the button we use to detect if PopMenu is still on-screen."""
|
|
|
|
def __enter__( self ):
|
|
|
|
webdriver.execute_script( "document.getElementById('popmenu-hack').style.display = 'block'" )
|
|
|
|
def __exit__( self, *args ):
|
|
|
|
webdriver.execute_script( "document.getElementById('popmenu-hack').style.display = 'none'" )
|
|
|
|
btn = find_child( "button#popmenu-hack", webdriver )
|
|
|
|
with PopMenuHack():
|
|
|
|
for i in range(0,10): #pylint: disable=unused-variable
|
|
|
|
try:
|
|
|
|
btn.click()
|
|
|
|
return
|
|
|
|
except WebDriverException:
|
|
|
|
time.sleep( 0.25 )
|
|
|
|
if find_child( ".ui-dialog", webdriver ):
|
|
|
|
return
|
|
|
|
assert False
|
|
|
|
|
|
|
|
def new_scenario():
|
|
|
|
"""Reset the scenario."""
|
|
|
|
select_menu_option( "new_scenario" )
|
|
|
|
# check if the webapp is asking for confirmation
|
|
|
|
if find_child( "#ask" ).is_displayed():
|
|
|
|
# yup - make it so
|
|
|
|
click_dialog_button( "OK" )
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def load_scenario_params( params ):
|
|
|
|
"""Load a full set of template parameters."""
|
|
|
|
for tab_id,fields in params.items():
|
|
|
|
select_tab( tab_id )
|
|
|
|
set_template_params( fields )
|
|
|
|
|
|
|
|
def set_template_params( params ): #pylint: disable=too-many-branches
|
|
|
|
"""Set template parameters."""
|
|
|
|
|
|
|
|
def add_sortable_entries( sortable, entries ):
|
|
|
|
"""Add simple notes to a sortable."""
|
|
|
|
for entry in entries:
|
|
|
|
add_simple_note( sortable, entry.get("caption",""), entry.get("width","") )
|
|
|
|
|
|
|
|
for key,val in params.items():
|
|
|
|
|
|
|
|
# check for scenario notes (these require special handling)
|
|
|
|
if key == "SCENARIO_NOTES":
|
|
|
|
# add them in (nb: we don't consider any existing scenario notes)
|
|
|
|
add_sortable_entries( find_child("#scenario_notes-sortable"), val )
|
|
|
|
continue
|
|
|
|
|
|
|
|
# check for SSR's (these require special handling)
|
|
|
|
if key == "SSR":
|
|
|
|
# add them in (nb: we don't consider any existing SSR's)
|
|
|
|
sortable = find_child( "#ssr-sortable" )
|
|
|
|
for ssr in val:
|
|
|
|
add_simple_note( sortable, ssr, None )
|
|
|
|
continue
|
|
|
|
|
|
|
|
# check for OB setups/notes (these require special handling)
|
|
|
|
if key in ("OB_SETUPS_1","OB_SETUPS_2","OB_NOTES_1","OB_NOTES_2"):
|
|
|
|
# add them in (nb: we don't consider any existing OB setup/note's)
|
|
|
|
mo = re.search( r"^(.*)_(\d)$", key )
|
|
|
|
sortable = find_child( "#{}-sortable_{}".format( mo.group(1).lower(), mo.group(2) ) )
|
|
|
|
add_sortable_entries( sortable, val )
|
|
|
|
continue
|
|
|
|
|
|
|
|
# check for vehicles/ordnance (these require special handling)
|
|
|
|
if key in ("OB_VEHICLES_1","OB_ORDNANCE_1","OB_VEHICLES_2","OB_ORDNANCE_2"):
|
|
|
|
# add them in (nb: we don't consider any existing vehicles/ordnance)
|
|
|
|
from vasl_templates.webapp.tests.test_vehicles_ordnance import add_vo #pylint: disable=cyclic-import
|
|
|
|
mo = re.search( r"^OB_(VEHICLES|ORDNANCE)_\d$", key )
|
|
|
|
vo_type = mo.group(1).lower()
|
|
|
|
for vo_name in val:
|
|
|
|
add_vo( _webdriver, vo_type, int(key[-1]), vo_name )
|
|
|
|
continue
|
|
|
|
|
|
|
|
# locate the next parameter
|
|
|
|
elem = next( c for c in ( \
|
|
|
|
find_child( "{}[name='{}']".format(elem_type,key) ) \
|
|
|
|
for elem_type in ["input","textarea","select"]
|
|
|
|
) if c )
|
|
|
|
|
|
|
|
# set the parameter value
|
|
|
|
if elem.tag_name == "select":
|
|
|
|
select_droplist_val( Select(elem), val )
|
|
|
|
else:
|
|
|
|
if elem.is_displayed():
|
|
|
|
elem.clear()
|
|
|
|
if val:
|
|
|
|
elem.send_keys( val )
|
|
|
|
if key == "SCENARIO_DATE":
|
|
|
|
elem.send_keys( Keys.TAB ) # nb: force the calendar popup to close :-/
|
|
|
|
wait_for( 5,
|
|
|
|
lambda: find_child( "#ui-datepicker-div" ).value_of_css_property( "display" ) == "none"
|
|
|
|
)
|
|
|
|
time.sleep( 0.25 )
|
|
|
|
else:
|
|
|
|
# FUDGE! Selenium can't interact with hidden elements, so we do it like this.
|
|
|
|
# However, we don't do this for everything since it doesn't always triggers events.
|
|
|
|
_webdriver.execute_script( "arguments[0].value = arguments[1]", elem, val )
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def set_scenario_date( scenario_date ):
|
|
|
|
"""Set the scenario date."""
|
|
|
|
if scenario_date is None:
|
|
|
|
return
|
|
|
|
select_tab( "scenario" )
|
|
|
|
elem = find_child( "input[name='SCENARIO_DATE']" )
|
|
|
|
elem.clear()
|
|
|
|
elem.send_keys( scenario_date )
|
|
|
|
elem.send_keys( Keys.TAB ) # nb: force the calendar popup to close :-/
|
|
|
|
|
|
|
|
def set_player( player_no, nat ):
|
|
|
|
"""Set a player's nationality."""
|
|
|
|
select_tab( "scenario" )
|
|
|
|
sel = Select( find_child( "select[name='PLAYER_{}']".format( player_no ) ) )
|
|
|
|
select_droplist_val( sel, nat )
|
|
|
|
return sel
|
|
|
|
|
|
|
|
def get_player_nat( player_no ):
|
|
|
|
"""Get a player's nationality."""
|
|
|
|
sel = Select( find_child( "select[name='PLAYER_{}']".format( player_no ) ) )
|
|
|
|
return sel.first_selected_option.get_attribute( "value" )
|
|
|
|
|
|
|
|
def set_theater( theater ):
|
|
|
|
"""Set the scenario theater."""
|
|
|
|
sel = Select( find_child( "select[name='SCENARIO_THEATER']" ) )
|
|
|
|
select_droplist_val( sel, theater )
|
|
|
|
|
|
|
|
def get_theater():
|
|
|
|
"""Get the scenario theater."""
|
|
|
|
sel = Select( find_child( "select[name='SCENARIO_THEATER']" ) )
|
|
|
|
return sel.first_selected_option.get_attribute( "value" )
|
|
|
|
|
|
|
|
def set_turn_track_nturns( nturns ):
|
|
|
|
"""Set the number of turns in the scenario (to enable the turn track)."""
|
|
|
|
elem = find_child( "#tabs-scenario select[name='TURN_TRACK_NTURNS']" )
|
|
|
|
select_droplist_val( Select(elem), nturns )
|
|
|
|
|
|
|
|
def get_turn_track_nturns():
|
|
|
|
"""Get the number of turns in the scenario."""
|
|
|
|
sel = Select( find_child( "#tabs-scenario select[name='TURN_TRACK_NTURNS']" ) )
|
|
|
|
return sel.first_selected_option.get_attribute( "value" )
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
_nationalities = None
|
|
|
|
|
|
|
|
def get_nationality_display_name( nat_id ):
|
|
|
|
"""Get the nationality's display name."""
|
|
|
|
global _nationalities
|
|
|
|
if not _nationalities:
|
|
|
|
_nationalities = get_nationalities( _webapp )
|
|
|
|
return _nationalities[ nat_id ]["display_name"]
|
|
|
|
|
|
|
|
def get_nationalities( webapp ):
|
|
|
|
"""Get the nationalities table."""
|
|
|
|
url = webapp.url_for( "get_template_pack" )
|
|
|
|
with urllib.request.urlopen( url ) as resp:
|
|
|
|
template_pack = json.load( resp )
|
|
|
|
return template_pack["nationalities"]
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
def add_simple_note( sortable, caption, width ):
|
|
|
|
"""Add a new simple note to a sortable."""
|
|
|
|
edit_simple_note( sortable, None, caption, width )
|
|
|
|
|
|
|
|
def edit_simple_note( sortable, entry_no, caption, width ):
|
|
|
|
"""Edit a simple note in a sortable."""
|
|
|
|
|
|
|
|
# figure out if we're creating a new entry, or editing an existing one
|
|
|
|
if entry_no is None:
|
|
|
|
# create a new entry
|
|
|
|
add_button = find_sortable_helper( sortable, "add" )
|
|
|
|
add_button.click()
|
|
|
|
else:
|
|
|
|
# edit an existing entry
|
|
|
|
elems = find_children( "li", sortable )
|
|
|
|
ActionChains(_webdriver).double_click( elems[entry_no] ).perform()
|
|
|
|
|
|
|
|
# edit the note
|
|
|
|
if caption is not None:
|
|
|
|
elem = find_child( "#edit-simple_note textarea" )
|
|
|
|
elem.clear()
|
|
|
|
elem.send_keys( caption )
|
|
|
|
if width is not None:
|
|
|
|
elem = find_child( ".ui-dialog-buttonpane input[name='width']" )
|
|
|
|
elem.clear()
|
|
|
|
elem.send_keys( width )
|
|
|
|
click_dialog_button( "OK" )
|
|
|
|
if caption == "":
|
|
|
|
# an empty caption will delete the entry - confirm the deletion
|
|
|
|
click_dialog_button( "OK" )
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def get_sortable_entry_text( sortable ):
|
|
|
|
"""Get the text for each entry in a sortable."""
|
|
|
|
return [ c.text for c in find_children("li", sortable) ]
|
|
|
|
|
|
|
|
def get_sortable_entry_count( sortable ):
|
|
|
|
"""Return the number of entries in a sortable."""
|
|
|
|
return len( find_children( "li", sortable ) )
|
|
|
|
|
|
|
|
def get_sortable_vo_names( sortable ):
|
|
|
|
"""Return the vehicle/ordnance names from a sortable."""
|
|
|
|
return [ c.text for c in find_children("li .vo-name",sortable) ]
|
|
|
|
|
|
|
|
def generate_sortable_entry_snippet( sortable, entry_no ):
|
|
|
|
"""Generate the snippet for a sortable entry."""
|
|
|
|
elems = find_children( "li img.snippet", sortable )
|
|
|
|
elems[entry_no].click()
|
|
|
|
return _get_clipboard()
|
|
|
|
|
|
|
|
def drag_sortable_entry_to_trash( sortable, entry_no ):
|
|
|
|
"""Draw a sortable entry to the trash."""
|
|
|
|
trash = find_sortable_helper( sortable, "trash" )
|
|
|
|
elems = find_children( "li", sortable )
|
|
|
|
ActionChains(_webdriver).drag_and_drop( elems[entry_no], trash ).perform()
|
|
|
|
|
|
|
|
def find_sortable_helper( sortable, tag ):
|
|
|
|
"""Find a sortable's helper element."""
|
|
|
|
sortable_id = sortable.get_attribute( "id" )
|
|
|
|
mo = re.search( r"^(.+)-sortable(_\d)?$", sortable_id )
|
|
|
|
helper_id = "#{}-{}".format( mo.group(1), tag )
|
|
|
|
if mo.group(2):
|
|
|
|
helper_id += mo.group(2)
|
|
|
|
return find_child( helper_id )
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
def get_stored_msg( msg_type, webdriver=None ):
|
|
|
|
"""Get a message stored for us by the front-end."""
|
|
|
|
if not webdriver:
|
|
|
|
webdriver = _webdriver
|
|
|
|
elem = find_child( "#" + msg_type, webdriver )
|
|
|
|
assert elem.tag_name == "textarea"
|
|
|
|
return elem.get_attribute( "value" )
|
|
|
|
|
|
|
|
def set_stored_msg( msg_type, val, webdriver=None ):
|
|
|
|
"""Set a message for the front-end."""
|
|
|
|
if not webdriver:
|
|
|
|
webdriver = _webdriver
|
|
|
|
elem = find_child( "#" + 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, webdriver=None ):
|
|
|
|
"""Store marker text in the message buffer (so we can tell if the front-end changes it)."""
|
|
|
|
# NOTE: Care should taken when using this function with "_clipboard_",
|
|
|
|
# since the tests might be using the real clipboard!
|
|
|
|
marker = "marker:{}:{}".format( msg_type, uuid.uuid4() )
|
|
|
|
set_stored_msg( msg_type, marker, webdriver )
|
|
|
|
return marker
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def find_child( sel, parent=None ):
|
|
|
|
"""Find a single child element."""
|
|
|
|
try:
|
|
|
|
# NOTE: I tried caching these results, but it didn't help the tests run any faster :-(
|
|
|
|
return (parent if parent else _webdriver).find_element( By.CSS_SELECTOR, sel )
|
|
|
|
except NoSuchElementException:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def find_children( sel, parent=None ):
|
|
|
|
"""Find child elements."""
|
|
|
|
try:
|
|
|
|
return (parent if parent else _webdriver).find_elements( By.CSS_SELECTOR, sel )
|
|
|
|
except NoSuchElementException:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def find_snippet_buttons( webdriver=None ):
|
|
|
|
"""Find all "generate snippet" buttons.
|
|
|
|
|
|
|
|
NOTE: We only return the 1st snippet button in the "extras" tab.
|
|
|
|
"""
|
|
|
|
snippet_btns = defaultdict( list )
|
|
|
|
# find all normal "generate snippet" buttons
|
|
|
|
for btn in find_children( "button.generate", webdriver ):
|
|
|
|
snippet_btns[ get_tab_for_elem(btn) ].append( btn )
|
|
|
|
# find "generate snippet" buttons in sortable lists
|
|
|
|
for btn in find_children( "ul.sortable img.snippet", webdriver ):
|
|
|
|
snippet_btns[ get_tab_for_elem(btn) ].append( btn )
|
|
|
|
# FUDGE! All nationality-specific buttons are created on each OB tab, and the ones not relevant
|
|
|
|
# to each player are just hidden. This is not real good since we have multiple elements with
|
|
|
|
# the same ID :-/ but we work around this by checking if the button is visible. Sigh...
|
|
|
|
snippet_btns2 = {}
|
|
|
|
for tab_id,btns in snippet_btns.items():
|
|
|
|
select_tab( tab_id, webdriver )
|
|
|
|
snippet_btns2[ tab_id ] = [ btn for btn in btns if btn.is_displayed() ]
|
|
|
|
return snippet_btns2
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def select_droplist_val( sel, val, isSelectMenu=False ):
|
|
|
|
"""Select a droplist option by value."""
|
|
|
|
_do_select_droplist( sel, val, isSelectMenu )
|
|
|
|
|
|
|
|
def select_droplist_index( sel, index, isSelectMenu=False ):
|
|
|
|
"""Select a droplist option by index."""
|
|
|
|
options = get_droplist_vals( sel )
|
|
|
|
_do_select_droplist( sel, options[index][0], isSelectMenu )
|
|
|
|
|
|
|
|
def _do_select_droplist( sel, val, isSelectMenu ):
|
|
|
|
"""Select a droplist option."""
|
|
|
|
sel_name = sel._el.get_attribute( "name" ) #pylint: disable=protected-access
|
|
|
|
elem = find_child( "select[name='{}']".format( sel_name ) )
|
|
|
|
_webdriver.execute_script( "$(arguments[0]).val( '{}' )".format( val ), elem )
|
|
|
|
if isSelectMenu:
|
|
|
|
# NOTE: jQuery's selectmenu component requires a slightly different trigger
|
|
|
|
_webdriver.execute_script( "$(arguments[0]).trigger( 'selectmenuchange' ).selectmenu( 'refresh' )", elem )
|
|
|
|
else:
|
|
|
|
_webdriver.execute_script( "$(arguments[0]).val( '{}' ).trigger( 'change' )".format( val ), elem )
|
|
|
|
|
|
|
|
def get_droplist_vals_index( sel ):
|
|
|
|
"""Get the value/text for each option in a droplist."""
|
|
|
|
return dict( get_droplist_vals( sel ) )
|
|
|
|
|
|
|
|
def get_droplist_vals( sel ):
|
|
|
|
"""Get the value/text for each option in a droplist."""
|
|
|
|
return [
|
|
|
|
( opt.get_attribute("value"), opt.get_attribute("text") )
|
|
|
|
for opt in sel.options
|
|
|
|
]
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def dismiss_notifications():
|
|
|
|
"""Dismiss all notifications."""
|
|
|
|
assert False, "Shouldn't need to call this function." # nb: notifications have been disabled during tests
|
|
|
|
while True:
|
|
|
|
elem = find_child( ".growl-close" )
|
|
|
|
if not elem:
|
|
|
|
break
|
|
|
|
try:
|
|
|
|
elem.click()
|
|
|
|
time.sleep( 0.25 )
|
|
|
|
except StaleElementReferenceException:
|
|
|
|
pass # nb: the notification had already auto-closed
|
|
|
|
|
|
|
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
def click_dialog_button( caption, parent=None ):
|
|
|
|
"""Click a dialog button."""
|
|
|
|
btns = [
|
|
|
|
elem for elem in find_children( ".ui-dialog button", parent )
|
|
|
|
if elem.text == caption
|
|
|
|
]
|
|
|
|
assert len(btns) == 1
|
|
|
|
btns[0].click()
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
_pyqt_app = None
|
|
|
|
|
|
|
|
def _get_clipboard() :
|
|
|
|
"""Get the contents of the clipboard.
|
|
|
|
|
|
|
|
NOTE: This used to be public, but there is sometimes a delay between doing something
|
|
|
|
in the UI (e.g. clicking a button) and the result appearing in the clipboard, so tests
|
|
|
|
should use wait_for_clipboard() instead.
|
|
|
|
"""
|
|
|
|
if vasl_templates.webapp.tests.pytest_options.use_clipboard:
|
|
|
|
global _pyqt_app
|
|
|
|
from PyQt5.QtWidgets import QApplication
|
|
|
|
if _pyqt_app is None:
|
|
|
|
_pyqt_app = QApplication( [] )
|
|
|
|
clipboard = QApplication.clipboard()
|
|
|
|
return clipboard.text()
|
|
|
|
else:
|
|
|
|
return get_stored_msg( "_clipboard_" )
|
|
|
|
|
|
|
|
def wait_for( timeout, func ):
|
|
|
|
"""Wait for a condition to become true."""
|
|
|
|
if os.name == "nt":
|
|
|
|
timeout *= 5 # Selenium runs pretty slow on Windows :-/
|
|
|
|
else:
|
|
|
|
timeout *= 2
|
|
|
|
start_time = time.time()
|
|
|
|
while True:
|
|
|
|
rc = func()
|
|
|
|
if rc:
|
|
|
|
return rc
|
|
|
|
assert time.time() - start_time < timeout
|
|
|
|
time.sleep( 0.1 )
|
|
|
|
|
|
|
|
def wait_for_elem( timeout, sel, parent=None ):
|
|
|
|
"""Wait for an element to appear ."""
|
|
|
|
args = { "elem": None }
|
|
|
|
def check_elem(): #pylint: disable=missing-docstring
|
|
|
|
args["elem"] = find_child( sel, parent )
|
|
|
|
return args["elem"] is not None and args["elem"].is_displayed()
|
|
|
|
wait_for( timeout, check_elem )
|
|
|
|
return args["elem"]
|
|
|
|
|
|
|
|
def wait_for_clipboard( timeout, expected, contains=None, transform=None ):
|
|
|
|
"""Wait for the clipboard to hold an expected value."""
|
|
|
|
args = { "last-clipboard": "" }
|
|
|
|
def check_clipboard(): #pylint: disable=missing-docstring
|
|
|
|
clipboard = _get_clipboard()
|
|
|
|
args["last-clipboard"] = clipboard
|
|
|
|
if transform:
|
|
|
|
clipboard = transform( clipboard )
|
|
|
|
if contains is None:
|
|
|
|
if isinstance( expected, typing.re.Pattern ):
|
|
|
|
return expected.search( clipboard ) is not None
|
|
|
|
else:
|
|
|
|
return expected == clipboard
|
|
|
|
elif contains is True:
|
|
|
|
return expected in clipboard
|
|
|
|
elif contains is False:
|
|
|
|
return expected not in clipboard
|
|
|
|
assert False
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
wait_for( timeout, check_clipboard )
|
|
|
|
return args["last-clipboard"]
|
|
|
|
except AssertionError:
|
|
|
|
print( "Timed out waiting for the clipboard:" )
|
|
|
|
print( "- Expecting:", expected )
|
|
|
|
print( "- Got:", args["last-clipboard"] )
|
|
|
|
raise
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
class SwitchFrame:
|
|
|
|
"""Helper class to switch frames in the browser window."""
|
|
|
|
def __init__( self, webdriver, iframe ):
|
|
|
|
self.webdriver = webdriver
|
|
|
|
self.iframe = iframe
|
|
|
|
def __enter__( self ):
|
|
|
|
if isinstance( self.iframe, str ):
|
|
|
|
self.iframe = wait_for( 5, lambda: find_child( self.iframe ) )
|
|
|
|
wait_for( 2, self.iframe.is_displayed )
|
|
|
|
self.webdriver.switch_to.frame( self.iframe )
|
|
|
|
return self
|
|
|
|
def __exit__( self, *args ):
|
|
|
|
self.webdriver.switch_to.default_content()
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
def unload_table( xpath, unload, html=None ):
|
|
|
|
"""Unload data from an HTML table."""
|
|
|
|
|
|
|
|
# NOTE: Extracting table data using Selenium is extremely slow, we use lxml for the win!
|
|
|
|
|
|
|
|
# initialize
|
|
|
|
doc = lxml.html.fromstring( html or _webdriver.page_source )
|
|
|
|
elems = doc.xpath( xpath )
|
|
|
|
if not elems:
|
|
|
|
return []
|
|
|
|
assert len(elems) == 1
|
|
|
|
table = elems[0]
|
|
|
|
|
|
|
|
def unload_cells( cells ):
|
|
|
|
"""Unload cell data from a table row."""
|
|
|
|
return [ "" if c.text is None else c.text.strip() for c in cells ]
|
|
|
|
|
|
|
|
# unload the table data
|
|
|
|
results = []
|
|
|
|
for row in table.xpath( "./tr" ):
|
|
|
|
if not results:
|
|
|
|
# we check for <th> in the first row only
|
|
|
|
cells = list( row.xpath( "./th" ) )
|
|
|
|
if cells:
|
|
|
|
results.append( unload_cells( cells ) )
|
|
|
|
continue
|
|
|
|
# extract the next row
|
|
|
|
cells = row.xpath( "./td" )
|
|
|
|
if unload:
|
|
|
|
cells = unload_cells( cells )
|
|
|
|
results.append( cells )
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
_IE_HTML_TAGS = [ "<i>" ]
|
|
|
|
|
|
|
|
def adjust_html( val ):
|
|
|
|
"""Adjust HTML content for IE."""
|
|
|
|
if vasl_templates.webapp.tests.pytest_options.webdriver != "ie":
|
|
|
|
return val
|
|
|
|
# convert HTML tags to uppercase :-/
|
|
|
|
for tag in _IE_HTML_TAGS:
|
|
|
|
val = val.replace( tag, tag.upper() )
|
|
|
|
assert tag.startswith( "<" ) and tag.endswith( ">" )
|
|
|
|
close_tag = "</{}".format( tag[1:] )
|
|
|
|
val = val.replace( close_tag, close_tag.upper() )
|
|
|
|
return val
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
def get_css_classes( elem ):
|
|
|
|
"""Get the CSS classes for the specified element."""
|
|
|
|
classes = elem.get_attribute( "class" )
|
|
|
|
return classes.split() if classes else []
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
|
|
|
|
def get_all_loggers():
|
|
|
|
"""Return all Python loggers."""
|
|
|
|
yield logging.getLogger() # nb: this is the root logger
|
|
|
|
for name in logging.root.manager.loggerDict: #pylint: disable=no-member
|
|
|
|
yield logging.getLogger( name )
|