Create attractive VASL scenarios, with loads of useful information embedded to assist with game play. https://vasl-templates.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
vasl-templates/vasl_templates/webapp/tests/utils.py

340 lines
12 KiB

""" Helper utilities. """
import os
import urllib.request
import json
import time
import re
import pytest
from PyQt5.QtWidgets import QApplication
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException
# standard templates
_STD_TEMPLATES = {
"scenario": [ "scenario", "players", "victory_conditions", "scenario_notes", "ssr" ],
"ob1": [ "ob_setup_1", "ob_note_1", "vehicles_1", "ordnance_1" ],
"ob2": [ "ob_setup_2", "ob_note_2", "vehicles_2", "ordnance_2" ],
}
# nationality-specific templates
_NAT_TEMPLATES = {
"german": [ "pf", "psk", "atmm" ],
"russian": [ "mol", "mol-p" ],
"american": [ "baz" ],
"british": [ "piat" ],
}
_webdriver = None
# ---------------------------------------------------------------------
def wait_for_page_ready():
"""Wait for the page to become ready."""
wait_for( 5, lambda: find_child("#_page-loaded_") is not None )
# ---------------------------------------------------------------------
def for_each_template( func ): #pylint: disable=too-many-branches
"""Test each template."""
# 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
templates_to_test.add( fname )
# test the standard templates
for tab_id,template_ids in _STD_TEMPLATES.items():
for template_id in template_ids:
select_tab( tab_id )
orig_template_id = template_id
if template_id == "scenario_notes":
template_id = "scenario_note"
elif template_id.startswith( "ob_setup_" ):
template_id = "ob_setup"
elif template_id.startswith( "ob_note_" ):
template_id = "ob_note"
elif template_id.startswith( "vehicles_" ):
template_id = "vehicles"
elif template_id.startswith( "ordnance_" ):
template_id = "ordnance"
func( template_id, orig_template_id )
if orig_template_id not in ("ob_setup_2","ob_note_2","vehicles_2","ordnance_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.
player1_sel = Select( find_child( "select[name='PLAYER_1']" ) )
for nat,template_ids in _NAT_TEMPLATES.items():
select_tab( "scenario" )
select_droplist_val( player1_sel, nat )
select_tab( "ob1" )
for template_id in template_ids:
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 ):
"""Select a tab in the main page."""
elem = find_child( "#tabs .ui-tabs-nav a[href='#tabs-{}']".format( tab_id ) )
elem.click()
def select_menu_option( menu_id ):
"""Select a menu option."""
elem = find_child( "#menu" )
elem.click()
elem = find_child( "a.PopMenu-Link[data-name='{}']".format( menu_id ) )
elem.click()
wait_for( 5, lambda: find_child("#menu .PopMenu-Container") is None ) # nb: wait for the menu to go away
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 ("VEHICLES_1","ORDNANCE_1","VEHICLES_2","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
vo_type = key[:key.index("_")].lower()
for vo_name in val:
add_vo( 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:
elem.clear()
if val:
elem.send_keys( val )
if key == "SCENARIO_DATE":
elem.send_keys( Keys.ESCAPE ) # 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 )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_nationalities( webapp ):
"""Get the nationalities table."""
url = webapp.url_for( "get_template_pack" )
template_pack = json.load( urllib.request.urlopen( url ) )
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 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_id ):
"""Get a message stored for us by the front-end."""
elem = find_child( "#"+msg_id )
if not elem:
return None
if elem.tag_name == "div":
return elem.text
assert elem.tag_name == "textarea"
return elem.get_attribute( "value" )
def set_stored_msg( msg_id, val ):
"""Set a message for the front-end."""
elem = find_child( "#"+msg_id )
assert elem.tag_name == "textarea"
_webdriver.execute_script( "arguments[0].value = arguments[1]", elem, val )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 select_droplist_val( sel, val ):
"""Select a droplist option by value."""
# get the options from the original <select>
sel_id = sel._el.get_attribute( "id" ) #pylint: disable=protected-access
options = get_droplist_vals( sel )
# open the jQuery droplist
elem = find_child( "#{}-button .ui-selectmenu-icon".format( sel_id ) )
elem.click()
# select the requested option (nb: clicking on the child option doesn't work :shrug:)
elem = find_child( "#{}-button".format( sel_id ) )
elem.send_keys( options[val] )
elem.send_keys( Keys.RETURN )
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."""
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 ):
"""Click a dialog button."""
btn = next(
elem for elem in find_children(".ui-dialog button")
if elem.text == caption
)
btn.click()
# ---------------------------------------------------------------------
_pyqt_app = None
def get_clipboard() :
"""Get the contents of the clipboard."""
if pytest.config.option.no_clipboard: #pylint: disable=no-member
return get_stored_msg( "_clipboard_" )
else:
global _pyqt_app
if _pyqt_app is None:
_pyqt_app = QApplication( [] )
clipboard = QApplication.clipboard()
return clipboard.text()
def wait_for( timeout, func ):
"""Wait for a condition to become true."""
start_time = time.time()
while True:
if func():
break
assert time.time() - start_time < timeout
time.sleep( 0.1 )