Exemple #1
0
def _analyze_vlogs(fnames):
    """Analyze log file(s)."""

    # initialize
    if isinstance(fnames, str):
        fnames = [fnames]
    select_menu_option("analyze_vlog")
    dlg = wait_for_elem(2, ".ui-dialog.lfa-upload")

    # add each log file
    for fno, fname in enumerate(fnames):
        fname = os.path.join(
            os.path.split(__file__)[0], "fixtures/analyze-vlog/" + fname)
        with open(fname, "rb") as fp:
            vlog_data = fp.read()
        set_stored_msg(
            "_vlog-persistence_", "{}|{}".format(
                os.path.split(fname)[1],
                base64.b64encode(vlog_data).decode("utf-8")))
        find_child("#lfa-upload .{}".format("hint" if fno == 0 else "files"),
                   dlg).click()
        wait_for(2, lambda: get_stored_msg("_vlog-persistence_") == "")

    # start the analysis
    find_child("button.ok", dlg).click()
    wait_for_elem(30, "#lfa")
Exemple #2
0
def save_scenario():
    """Save the scenario."""
    marker = set_stored_msg_marker("_scenario-persistence_")
    select_menu_option("save_scenario")
    wait_for(2, lambda: get_stored_msg("_scenario-persistence_") != marker)
    data = get_stored_msg("_scenario-persistence_")
    return json.loads(data)
Exemple #3
0
def load_scenario(scenario, webdriver=None):
    """Load a scenario into the UI."""
    set_stored_msg("_scenario-persistence_", json.dumps(scenario), webdriver)
    _ = set_stored_msg_marker("_last-info_", webdriver)
    select_menu_option("load_scenario", webdriver)
    wait_for(
        2, lambda: get_stored_msg("_last-info_", webdriver) ==
        "The scenario was loaded.")
Exemple #4
0
def test_include_flags_in_snippets(webapp, webdriver):
    """Test including flags in snippets."""

    # initialize
    webapp.control_tests.set_data_dir("{REAL}")
    init_webapp(webapp, webdriver)

    # prepare the scenario
    set_player(1, "german")
    select_tab("ob1")
    sortable = find_child("#ob_setups-sortable_1")
    add_simple_note(sortable, "OB setup note", None)

    # enable "show flags in snippets"
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='include-flags-in-snippets']")
    assert not elem.is_selected()
    elem.click()
    click_dialog_button("OK")
    _check_cookies(webdriver, "include-flags-in-snippets", True)

    # make sure that it took effect
    ob_setup_snippet_btn = find_child("li img.snippet", sortable)
    ob_setup_snippet_btn.click()
    wait_for_clipboard(2, "/flags/german", contains=True)

    # make sure it also affects vehicle/ordnance snippets
    ob_vehicles_snippet_btn = find_child(
        "button.generate[data-id='ob_vehicles_1']")
    ob_vehicles_snippet_btn.click()
    wait_for_clipboard(2, "/flags/german", contains=True)
    ob_ordnance_snippet_btn = find_child(
        "button.generate[data-id='ob_ordnance_1']")
    ob_ordnance_snippet_btn.click()
    wait_for_clipboard(2, "/flags/german", contains=True)

    # disable "show flags in snippets"
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='include-flags-in-snippets']")
    assert elem.is_selected()
    elem.click()
    click_dialog_button("OK")
    _check_cookies(webdriver, "include-flags-in-snippets", False)

    # make sure that it took effect
    ob_setup_snippet_btn.click()
    wait_for_clipboard(2, "/flags/german", contains=False)

    # make sure it also affects vehicle/ordnance snippets
    ob_vehicles_snippet_btn.click()
    wait_for_clipboard(2, "/flags/german", contains=False)
    ob_ordnance_snippet_btn.click()
    wait_for_clipboard(2, "/flags/german", contains=False)
Exemple #5
0
def _enable_vo_no_notes_as_images(enable):
    """Enable/disable vehicle/ordnance notes as images."""
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='vo-notes-as-images']")
    if (elem.is_selected() and not enable) or (not elem.is_selected()
                                               and enable):
        elem.click()
        click_dialog_button("OK")
    else:
        click_dialog_button("Cancel")
Exemple #6
0
def test_vo_notes_as_images(webapp, webdriver):
    """Test showing vehicle/ordnance notes as HTML/images."""

    # initialize
    webapp.control_tests.set_vo_notes_dir("{TEST}")
    init_webapp(webapp, webdriver, scenario_persistence=1)

    # load the test vehicle
    load_scenario({
        "PLAYER_1": "greek",
        "OB_VEHICLES_1": [{
            "name": "HTML note"
        }],
    })
    select_tab("ob1")

    def check_snippet(expected):
        """Generate and check the vehicle note snippet."""
        sortable = find_child("#ob_vehicles-sortable_1")
        elems = find_children("li", sortable)
        assert len(elems) == 1
        btn = find_child("img.snippet", elems[0])
        btn.click()
        contains = True if isinstance(expected, str) else None
        wait_for_clipboard(2, expected, contains=contains)

    # generate the vehicle snippet (should get the raw HTML)
    check_snippet("This is an HTML vehicle note (202).")

    # enable "show vehicle/ordnance notes as images"
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='vo-notes-as-images']")
    assert not elem.is_selected()
    elem.click()
    click_dialog_button("OK")
    _check_cookies(webdriver, "vo-notes-as-images", True)

    # generate the vehicle snippet (should get a link to return an image)
    check_snippet(re.compile(r"http://.+?:\d+/vehicles/greek/note/202"))

    # disable "show vehicle/ordnance notes as images"
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='vo-notes-as-images']")
    assert elem.is_selected()
    elem.click()
    click_dialog_button("OK")
    _check_cookies(webdriver, "vo-notes-as-images", False)

    # generate the vehicle snippet (should get the raw HTML)
    check_snippet("This is an HTML vehicle note (202).")
Exemple #7
0
def test_hide_unavailable_ma_notes(webapp, webdriver):
    """Test showing/hiding unavailable multi-applicable notes."""

    # initialize
    webapp.control_tests.set_vo_notes_dir("{TEST}")
    init_webapp(webapp, webdriver, scenario_persistence=1)

    # load the test vehicle
    load_scenario({
        "PLAYER_1": "german",
        "OB_VEHICLES_1": [{
            "name": "missing multi-applicable note"
        }]
    })
    select_tab("ob1")

    def test_ma_notes(ma_note_q_present):  #pylint: disable=missing-docstring
        expected = [("A", 'German Multi-Applicable Vehicle Note "A".')]
        if ma_note_q_present:
            expected.append(("Q", "Unavailable."))
        btn = find_child("button[data-id='ob_vehicles_ma_notes_1']")
        btn.click()
        wait_for_clipboard(2, expected, transform=extract_ma_notes)

    # generate the multi-applicable notes
    test_ma_notes(True)

    # enable "hide unavailable multi-applicable notes"
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='hide-unavailable-ma-notes']")
    assert not elem.is_selected()
    elem.click()
    click_dialog_button("OK")
    _check_cookies(webdriver, "hide-unavailable-ma-notes", True)

    # generate the multi-applicable notes
    test_ma_notes(False)

    # disable "hide unavailable multi-applicable notes"
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='hide-unavailable-ma-notes']")
    assert elem.is_selected()
    elem.click()
    click_dialog_button("OK")
    _check_cookies(webdriver, "hide-unavailable-ma-notes", False)

    # generate the multi-applicable notes
    test_ma_notes(True)
Exemple #8
0
def set_user_settings(opts):
    """Configure the user settings."""
    select_menu_option("user_settings")
    for key, val in opts.items():
        if isinstance(val, bool):
            elem = find_child(
                ".ui-dialog.user-settings input[name='{}']".format(key))
            if (val and not elem.is_selected()) or (not val
                                                    and elem.is_selected()):
                elem.click()
        elif isinstance(val, int):
            elem = find_child(
                ".ui-dialog.user-settings select[name='{}']".format(key))
            select_droplist_val(Select(elem), val)
        else:
            assert False
    click_dialog_button("OK")
Exemple #9
0
def _do_upload_template_pack(data, error_expected):
    """Upload a template pack."""

    # upload the template pack
    set_stored_msg("_template-pack-persistence_", data)
    info_marker = set_stored_msg_marker("_last-info_")
    error_marker = set_stored_msg_marker("_last-error_")
    select_menu_option("template_pack")

    # wait for the front-end to finish
    if error_expected:
        func = lambda: get_stored_msg("_last-error_") != error_marker
    else:
        func = lambda: "was loaded" in get_stored_msg("_last-info_")
    wait_for(2, func)

    return info_marker, error_marker
def _update_vsav(fname, expected):
    """Update a VASL scenario."""

    # read the VSAV data
    with open(fname, "rb") as fp:
        vsav_data = fp.read()

    # send the VSAV data to the front-end to be updated
    set_stored_msg("_vsav-persistence_",
                   base64.b64encode(vsav_data).decode("utf-8"))
    _ = set_stored_msg_marker("_last-info_")
    _ = set_stored_msg_marker("_last-warning_")
    select_menu_option("update_vsav")

    # wait for the front-end to receive the data
    def check_response():
        # NOTE: If something is misconfigured, the error response can get stored in the persistence buffer
        # really quickly i.e. before we get a chance to detect it here being cleared first.
        resp = get_stored_msg("_vsav-persistence_")
        return resp == "" or resp.startswith("ERROR:")

    wait_for(2, check_response)

    # wait for the updated data to come back
    timeout = 120 if os.name == "nt" else 60
    wait_for(timeout, lambda: get_stored_msg("_vsav-persistence_") != "")
    updated_vsav_data = get_stored_msg("_vsav-persistence_")
    if updated_vsav_data.startswith("ERROR: "):
        raise RuntimeError(updated_vsav_data)
    updated_vsav_data = base64.b64decode(updated_vsav_data)

    # parse the VASSAL shim report
    if expected:
        report = {}
        msg = get_stored_msg("_last-warning_" if "deleted" in
                             expected else "_last-info_")
        assert "The VASL scenario was updated:" in msg
        for mo2 in re.finditer("<li>([^<]+)", msg):
            mo3 = re.search(r"^(\d+) labels? (were|was) ([a-z]+)",
                            mo2.group(1))
            report[mo3.group(3)] = int(mo3.group(1))
        assert report == expected
    else:
        assert "No changes were made" in get_stored_msg("_last-info_")

    return updated_vsav_data
Exemple #11
0
def test_help(webapp, webdriver):
    """Test the help page."""

    # initialize
    init_webapp(webapp, webdriver)

    # make sure the HELP tab is not visible
    def get_tabs():
        """Get the visible tabs."""
        return [
            c.get_attribute("aria-controls")
            for c in find_children("#tabs .ui-tabs-tab") if c.is_displayed()
        ]

    assert "tabs-help" not in get_tabs()

    # show the help
    select_menu_option("show_help")

    # make sure that the HELP tab is now visible
    assert "tabs-help" in get_tabs()

    # check what's in the help iframe
    try:

        # switch to the frame
        webdriver.switch_to.frame(find_child("#tabs-help iframe"))

        # check that the content loaded OK
        assert "everyone's favorite scenario" in webdriver.page_source

        # check that the license loaded OK
        elem = wait_for_elem(2, "a.ui-tabs-anchor[href='#helptabs-license']")
        assert elem.is_displayed()
        wait_for(
            2, lambda: "GNU AFFERO GENERAL PUBLIC LICENSE" in webdriver.
            page_source)
        assert "Version 3" in webdriver.page_source

    finally:

        # switch back to the main window
        webdriver.switch_to.default_content()
Exemple #12
0
def test_include_vasl_images_in_snippets(webapp, webdriver):
    """Test including VASL counter images in snippets."""

    # initialize
    webapp.control_tests.set_data_dir("{REAL}")
    init_webapp(webapp, webdriver)
    set_user_settings(
        {"scenario-images-source": SCENARIO_IMAGES_SOURCE_THIS_PROGRAM})

    # add a vehicle
    set_player(1, "german")
    add_vo(webdriver, "vehicles", 1, "PzKpfw IB")

    # enable "show VASL images in snippets"
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='include-vasl-images-in-snippets']"
    )
    assert not elem.is_selected()
    elem.click()
    click_dialog_button("OK")
    _check_cookies(webdriver, "include-vasl-images-in-snippets", True)

    # make sure that it took effect
    snippet_btn = find_child("button[data-id='ob_vehicles_1']")
    snippet_btn.click()
    wait_for_clipboard(2, "/counter/2524/front", contains=True)

    # disable "show VASL images in snippets"
    select_menu_option("user_settings")
    elem = find_child(
        ".ui-dialog.user-settings input[name='include-vasl-images-in-snippets']"
    )
    assert elem.is_selected()
    elem.click()
    click_dialog_button("OK")
    _check_cookies(webdriver, "include-vasl-images-in-snippets", False)

    # make sure that it took effect
    snippet_btn.click()
    wait_for_clipboard(2, "/counter/2524/front", contains=False)
def analyze_vsav(fname, expected_ob1, expected_ob2, expected_report):
    """Analyze a VASL scenario."""

    # read the VSAV data
    fname = os.path.join(
        os.path.split(__file__)[0], "fixtures/analyze-vsav/" + fname)
    with open(fname, "rb") as fp:
        vsav_data = fp.read()

    # send the VSAV data to the front-end to be analyzed
    set_stored_msg("_vsav-persistence_",
                   base64.b64encode(vsav_data).decode("utf-8"))
    prev_info_msg = set_stored_msg_marker("_last-info_")
    set_stored_msg_marker("_last-warning_")
    select_menu_option("analyze_vsav")

    # wait for the analysis to finish
    wait_for(2, lambda: find_child("#please-wait").is_displayed())
    wait_for(60, lambda: not find_child("#please-wait").is_displayed())

    # check the results
    saved_scenario = save_scenario()

    def get_ids(key):  #pylint: disable=missing-docstring
        return set(
            (v["id"], v.get("image_id")) for v in saved_scenario.get(key, []))

    def adjust_expected(vals):  #pylint: disable=missing-docstring
        return set(v if isinstance(v, tuple) else (v, None) for v in vals)

    assert get_ids("OB_VEHICLES_1") == adjust_expected(expected_ob1[0])
    assert get_ids("OB_ORDNANCE_1") == adjust_expected(expected_ob1[1])
    assert get_ids("OB_VEHICLES_2") == adjust_expected(expected_ob2[0])
    assert get_ids("OB_ORDNANCE_2") == adjust_expected(expected_ob2[1])

    # check the report
    msg = get_stored_msg("_last-info_")
    if msg == prev_info_msg:
        msg = get_stored_msg("_last-warning_")
    assert all(e in msg for e in expected_report)
Exemple #14
0
    def do_test(tab_id, param):
        """Test checking for a dirty scenario."""

        # change the specified field
        check_is_dirty(False)
        select_tab(tab_id)
        state = change_field(param)
        check_is_dirty(True)

        # make sure we get asked to confirm a "new scenario" operation
        select_menu_option("new_scenario")
        wait_for(2, lambda: find_child("#ask") is not None)
        elem = find_child("#ask")
        assert "This scenario has been changed" in elem.text

        # cancel the confirmation request, make sure the change we made is still there
        click_dialog_button("Cancel")
        select_tab(tab_id)
        check_field(param, state)
        check_is_dirty(True)

        # revert the change
        revert_field(param, state)
        check_is_dirty(False)

        # we should now be able to reset the scenario without a confirmation
        _ = set_stored_msg_marker("_last-info_")
        select_menu_option("new_scenario")
        wait_for(
            2,
            lambda: get_stored_msg("_last-info_") == "The scenario was reset.")

        # change the field again
        select_tab(tab_id)
        state = change_field(param)
        check_is_dirty(True)

        # make sure we get asked to confirm a "load scenario" operation
        select_menu_option("load_scenario")
        wait_for(2, lambda: find_child("#ask") is not None)
        elem = find_child("#ask")
        assert "This scenario has been changed" in elem.text

        # cancel the confirmation request, make sure the change we made is still there
        click_dialog_button("Cancel")
        select_tab(tab_id)
        check_field(param, state)
        check_is_dirty(True)

        # revert the change
        revert_field(param, state)
        check_is_dirty(False)
Exemple #15
0
def test_elite( webapp, webdriver ): #pylint: disable=too-many-statements
    """Test elite vehicles/ordnance."""

    # initialize
    webapp.control_tests.set_data_dir( "{REAL}" )
    init_webapp( webapp, webdriver, scenario_persistence=1 )

    def get_sortable_elem():
        """Find the sortable element for the test vehicle."""
        sortable = find_child( "#ob_vehicles-sortable_1" )
        elems = find_children( "li", sortable )
        assert len(elems) == 1
        return elems[0]
    def check_elite( expected, custom ):
        """Check the elite status of the vehicle in the main UI."""
        vo_name = find_child( ".vo-name", get_sortable_elem() ).text
        caps = [ c.text for c in find_children(".vo-capability",get_sortable_elem()) ]
        if expected:
            assert vo_name.endswith( "\u24ba" )
            expected = [ "H9", "s10", "sD7", "CS 5" ]
            if custom:
                expected.append( "HE11" )
            assert caps == expected
        else:
            assert "\u24ba" not in vo_name
            expected = [ "H8", "s9", "sD7", "CS 5" ]
            if custom:
                expected.append( "HE10" )
            assert caps == expected
    def check_elite2( expected, custom ):
        """Check the elite status of the vehicle in the edit dialog."""
        vo_name = find_child( "#edit-vo .header .vo-name" ).text
        caps = [ c.get_attribute("value") for c in find_children("#vo_capabilities-sortable input[type='text']") ]
        if expected:
            assert vo_name.endswith( "\u24ba" )
            expected = [ "H9", "s10", "sD7", "CS 5" ]
            if custom:
                expected.append( "HE11" )
            assert caps == expected
        else:
            assert "\u24ba" not in vo_name
            expected = [ "H8", "s9", "sD7", "CS 5" ]
            if custom:
                expected.append( "HE10" )
            assert caps == expected

    # load the scenario
    scenario_data = {
        "PLAYER_1": "german",
        "OB_VEHICLES_1": [ { "name": "PSW 233" } ], # H8 s9 sD7 CS 5
    }
    load_scenario( scenario_data )
    select_tab( "ob1" )

    # check that the vehicle was loaded non-elite
    check_elite( False, False )

    # add a custom capability
    ActionChains( webdriver ).double_click( get_sortable_elem() ).perform()
    elem = find_child( "#vo_capabilities-add" )
    elem.click()
    elems = find_children( "#vo_capabilities-sortable input[type='text']" )
    assert len(elems) == 5
    elems[4].send_keys( "HE10" )
    click_dialog_button( "OK" )

    # make the vehicle elite
    ActionChains( webdriver ).double_click( get_sortable_elem() ).perform()
    check_elite2( False, True )
    elem = find_child( "#edit-vo .capabilities .elite" )
    elem.click()
    check_elite2( True, True )
    click_dialog_button( "OK" )
    check_elite( True, True )

    # save the scenario, then reload it
    saved_scenario = save_scenario()
    assert len(saved_scenario["OB_VEHICLES_1"]) == 1
    assert saved_scenario["OB_VEHICLES_1"][0]["elite"]
    assert saved_scenario["OB_VEHICLES_1"][0]["custom_capabilities"] == \
        [ "H9", "s10", "sD7", "CS 5", "HE11" ]
    select_menu_option( "new_scenario" )
    load_scenario( saved_scenario )
    select_tab( "ob1" )
    check_elite( True, True )

    # make the vehicle non-elite
    ActionChains( webdriver ).double_click( get_sortable_elem() ).perform()
    check_elite2( True, True )
    elem = find_child( "#edit-vo .capabilities .elite" )
    elem.click()
    check_elite2( False, True )
    click_dialog_button( "OK" )
    check_elite( False, True )

    # save the scenario
    saved_scenario = save_scenario()
    assert len(saved_scenario["OB_VEHICLES_1"]) == 1
    assert "elite" not in saved_scenario["OB_VEHICLES_1"][0]
    assert saved_scenario["OB_VEHICLES_1"][0]["custom_capabilities"] == \
        [ "H8", "s9", "sD7", "CS 5", "HE10" ]

    # make the vehicle elite, remove the custom capability
    ActionChains( webdriver ).double_click( get_sortable_elem() ).perform()
    check_elite2( False, True )
    elem = find_child( "#edit-vo .capabilities .elite" )
    elem.click()
    check_elite2( True, True )
    elems = find_children( "#vo_capabilities-sortable li" )
    webdriver.execute_script( "arguments[0].scrollIntoView(true);", elems[4] )
    ActionChains( webdriver ).key_down( Keys.CONTROL ).click( elems[4] ).perform()
    ActionChains( webdriver ).key_up( Keys.CONTROL ).perform()
    click_dialog_button( "OK" )
    check_elite( True, False )

    # save the scenario, then reload it
    saved_scenario = save_scenario()
    assert len(saved_scenario["OB_VEHICLES_1"]) == 1
    assert saved_scenario["OB_VEHICLES_1"][0]["elite"]
    assert saved_scenario["OB_VEHICLES_1"][0]["custom_capabilities"] == [ "H9", "s10", "sD7", "CS 5" ]
    select_menu_option( "new_scenario" )
    load_scenario( saved_scenario )
    select_tab( "ob1" )
    check_elite( True, False )

    # make the vehicle non-elite
    ActionChains( webdriver ).double_click( get_sortable_elem() ).perform()
    check_elite2( True, False )
    elem = find_child( "#edit-vo .capabilities .elite" )
    elem.click()
    check_elite2( False, False )
    click_dialog_button( "OK" )
    check_elite( False, False )

    # save the scenario
    saved_scenario = save_scenario()
    assert len(saved_scenario["OB_VEHICLES_1"]) == 1
    assert "elite" not in saved_scenario["OB_VEHICLES_1"][0]
    assert "custom_capabilities" not in saved_scenario["OB_VEHICLES_1"][0]
Exemple #16
0
def test_change_vo_image( webapp, webdriver ):
    """Test changing a V/O image."""

    # initialize
    webapp.control_tests \
        .set_data_dir( "{REAL}" ) \
        .set_vasl_version( "random", None )
    init_webapp( webapp, webdriver, scenario_persistence=1 )

    # add an ISU-152
    set_player( 2, "russian" )
    add_vo( webdriver, "vehicles", 2, "ISU-152" )

    # save the scenario
    saved_scenario = save_scenario()
    assert saved_scenario["OB_VEHICLES_2"] ==  [ { "id": "ru/v:049", "name": "ISU-152", "seq_id": 1 } ]

    # change the vehicle's image
    vehicles_sortable = find_child( "#ob_vehicles-sortable_2" )
    elems = find_children( "li", vehicles_sortable )
    assert len(elems) == 1
    ActionChains(webdriver).double_click( elems[0] ).perform()
    img = find_child( "#edit-vo img.vasl-image" )
    assert img.get_attribute( "src" ).endswith( "/counter/657/front" )
    btn = find_child( "#edit-vo input.select-vo-image" )
    btn.click()
    images = find_children( ".ui-dialog.select-vo-image .vo-images img" )
    assert len(images) == 2
    images[1].click()
    assert img.get_attribute( "src" ).endswith( "/counter/659/front/0" )
    click_dialog_button( "OK" )
    elems = find_children( "img.vasl-image", vehicles_sortable )
    assert len(elems) == 1
    assert elems[0].get_attribute( "src" ).endswith( "/counter/659/front/0" )

    # save the scenario
    saved_scenario = save_scenario()
    assert saved_scenario["OB_VEHICLES_2"] ==  [
        { "id": "ru/v:049", "image_id": "659/0", "name": "ISU-152", "seq_id": 1 }
    ]

    # reload the scenario, and check the vehicle's image
    select_menu_option( "new_scenario" )
    load_scenario( saved_scenario )
    select_tab( "ob2" )
    elems = find_children( "img.vasl-image", vehicles_sortable )
    assert len(elems) == 1
    assert elems[0].get_attribute( "src" ).endswith( "/counter/659/front/0" )

    # change the vehicle's image back to the default
    elems = find_children( "li", vehicles_sortable )
    assert len(elems) == 1
    ActionChains(webdriver).double_click( elems[0] ).perform()
    img = find_child( "#edit-vo img.vasl-image" )
    assert img.get_attribute( "src" ).endswith( "/counter/659/front/0" )
    btn = find_child( "#edit-vo input.select-vo-image" )
    btn.click()
    images = find_children( ".ui-dialog.select-vo-image .vo-images img" )
    assert len(images) == 2
    images[0].click()
    assert img.get_attribute( "src" ).endswith( "/counter/657/front/0" )
    click_dialog_button( "OK" )
    elems = find_children( "img.vasl-image", vehicles_sortable )
    assert len(elems) == 1
    assert elems[0].get_attribute( "src" ).endswith( "/counter/657/front/0" )

    # save the scenario
    saved_scenario = save_scenario()
    assert saved_scenario["OB_VEHICLES_2"] ==  [
        { "id": "ru/v:049", "image_id": "657/0", "name": "ISU-152", "seq_id": 1 }
    ]
Exemple #17
0
def test_custom_comments( webapp, webdriver ): #pylint: disable=too-many-statements
    """Test custom comments."""

    # NOTE: Vehicle/ordnance comments are not capabilities, but they are managed in the same place
    # and the code is virtually identical, so it makes sense to put the test code here.

    # initialize
    init_webapp( webapp, webdriver, scenario_persistence=1 )

    # add a vehicle
    add_vo( webdriver, "vehicles", 1, "a commented german vehicle" )

    snippet_btn = find_child( "button[data-id='ob_vehicles_1']" )
    def extract_comments( clipboard ):
        """Extract the comments."""
        mo = re.search( r"^- comments: (.*)$", clipboard, re.MULTILINE )
        return mo.group(1) if mo else ""
    def check_snippet( expected ):
        """Check the vehicle's snippet."""
        snippet_btn.click()
        wait_for_clipboard( 2, expected, transform=extract_comments )
    def check_comments_in_dialog( expected ):
        """Check the vehicle's comments."""
        elems = find_children( "#vo_comments-sortable li" )
        elems2 = [ find_child("input[type='text']",c) for c in elems ]
        assert [ e.get_attribute("value") for e in elems2 ] == expected
        return elems

    # check the vehicle's snippet
    check_snippet( '"a comment" "another comment"' )

    # edit the vehicle's comments
    vehicles_sortable = find_child( "#ob_vehicles-sortable_1" )
    elems = find_children( "li", vehicles_sortable )
    assert len(elems) == 1
    ActionChains( webdriver ).double_click( elems[0] ).perform()
    elems = check_comments_in_dialog( [ "a comment", "another comment" ] )

    # edit one of the comments
    elem = find_child( "input[type='text']", elems[0] )
    elem.clear()
    elem.send_keys( "a comment (modified)" )

    # delete a comment
    ActionChains( webdriver ).key_down( Keys.CONTROL ).click( elems[1] ).perform()
    ActionChains( webdriver ).key_up( Keys.CONTROL ).perform()

    # add a new comment
    elem = find_child( "#vo_comments-add" )
    elem.click()
    elems = find_children( "#vo_comments-sortable input[type='text']" )
    assert len(elems) == 2
    elems[1].send_keys( "a <i>new</i> comment" )

    # save the changes and check the vehicle's snippet
    click_dialog_button( "OK" )
    check_snippet( '"a comment (modified)" "a <i>new</i> comment"' )

    # save the scenario
    saved_scenario = save_scenario()
    assert len(saved_scenario["OB_VEHICLES_1"]) == 1
    assert saved_scenario["OB_VEHICLES_1"][0]["custom_comments"] == [ "a comment (modified)", "a <i>new</i> comment" ]

    # reload the scenario, and check the vehicle's snippet
    select_menu_option( "new_scenario" )
    load_scenario( saved_scenario )
    select_tab( "ob1" )
    check_snippet( '"a comment (modified)" "a <i>new</i> comment"' )

    # make sure the comments are loaded correcly when editing the vehicle
    elems = find_children( "li", vehicles_sortable )
    assert len(elems) == 1
    ActionChains( webdriver ).double_click( elems[0] ).perform()
    elems = check_comments_in_dialog( [ "a comment (modified)", "a <i>new</i> comment" ] )

    # delete all comments
    for elem in elems:
        ActionChains( webdriver ).key_down( Keys.CONTROL ).click( elem ).perform()
        ActionChains( webdriver ).key_up( Keys.CONTROL ).perform()
    click_dialog_button( "OK" )
    check_snippet( "" )

    # save the scenario
    saved_scenario2 = save_scenario()
    assert len(saved_scenario2["OB_VEHICLES_1"]) == 1
    assert saved_scenario2["OB_VEHICLES_1"][0]["custom_comments"] == []

    # reload the scenario, and reset the vehicle's comments back to the default
    load_scenario( saved_scenario )
    select_tab( "ob1" )
    elems = find_children( "li", vehicles_sortable )
    assert len(elems) == 1
    ActionChains( webdriver ).double_click( elems[0] ).perform()
    btn = find_child( "#vo_comments-reset" )
    btn.click()
    click_dialog_button( "OK" )
    check_snippet( '"a comment" "another comment"' )

    # make sure the custom comments are no longer saved in the scenario
    saved_scenario2 = save_scenario()
    assert len(saved_scenario2["OB_VEHICLES_1"]) == 1
    assert "custom_comments" not in saved_scenario2["OB_VEHICLES_1"][0]

    # reload the scenario, and manually set the vehicle's comments to be the same as the default
    load_scenario( saved_scenario )
    select_tab( "ob1" )
    elems = find_children( "li", vehicles_sortable )
    assert len(elems) == 1
    ActionChains( webdriver ).double_click( elems[0] ).perform()
    elems = find_children( "#vo_comments-sortable input[type='text']" )
    assert len(elems) == 2
    elems[0].clear()
    elems[0].send_keys( "a comment" )
    elems[1].clear()
    elems[1].send_keys( "another comment" )
    click_dialog_button( "OK" )

    # make sure the custom comments are no longer saved in the scenario
    saved_scenario = save_scenario()
    assert len(saved_scenario["OB_VEHICLES_1"]) == 1
    assert "custom_comments" not in saved_scenario["OB_VEHICLES_1"][0]
def update_vsav_thread(webapp_url, vsav_fname, vsav_data):
    """Test updating VASL scenario files."""

    # initialize
    vsav_data_b64 = base64.b64encode(vsav_data).decode("utf-8")

    with WebDriver() as webdriver:

        # initialize
        webdriver = webdriver.driver
        init_webapp(webdriver, webapp_url,
                    ["vsav_persistence", "scenario_persistence"])

        # load a test scenario
        fname = os.path.join(
            os.path.split(__file__)[0],
            "../webapp/tests/fixtures/update-vsav/full.json")
        with open(fname, "r", encoding="utf-8") as fp:
            saved_scenario = json.load(fp)
        load_scenario(saved_scenario, webdriver)

        while not shutdown_event.is_set():

            try:

                # send the VSAV data to the front-end to be updated
                log("Updating VSAV: {}", vsav_fname)
                set_stored_msg("_vsav-persistence_", vsav_data_b64, webdriver)
                select_menu_option("update_vsav", webdriver)
                start_time = time.time()

                # wait for the front-end to receive the data
                wait_for(
                    2 * thread_count, lambda: get_stored_msg(
                        "_vsav-persistence_", webdriver) == "")

                # wait for the updated data to arrive
                wait_for(
                    60 * thread_count, lambda: get_stored_msg(
                        "_vsav-persistence_", webdriver) != "")
                elapsed_time = time.time() - start_time

                # get the updated VSAV data
                updated_vsav_data = get_stored_msg("_vsav-persistence_",
                                                   webdriver)
                if updated_vsav_data.startswith("ERROR: "):
                    raise RuntimeError(updated_vsav_data)
                updated_vsav_data = base64.b64decode(updated_vsav_data)

                # check the updated VSAV
                log("Received updated VSAV data: #bytes={}",
                    len(updated_vsav_data))
                assert updated_vsav_data[:2] == b"PK"

                # update the stats
                with stats_lock:
                    stats["update vsav"][0] += 1
                    stats["update vsav"][1] += elapsed_time

            except (ConnectionRefusedError, ConnectionResetError,
                    http.client.RemoteDisconnected):
                if shutdown_event.is_set():
                    break
                raise
Exemple #19
0
def test_scenario_persistence(webapp, webdriver):  #pylint: disable=too-many-statements,too-many-locals,too-many-branches
    """Test loading/saving scenarios."""

    # initialize
    init_webapp(webapp, webdriver, scenario_persistence=1)

    def check_ob_tabs(*args):
        """Check that the OB tabs have been set correctly."""
        for player_no in [1, 2]:
            elem = find_child(
                "#tabs .ui-tabs-nav a[href='#tabs-ob{}']".format(player_no))
            nat = args[player_no - 1]
            assert elem.text.strip() == "{} OB".format(
                get_nationality_display_name(nat))

    def check_window_title(expected):
        """Check the window title."""
        if expected:
            expected = "{} - {}".format(APP_NAME, expected)
        else:
            expected = APP_NAME
        assert webdriver.title == expected

    # load the scenario fields
    SCENARIO_PARAMS = {
        "scenario": {
            "SCENARIO_NAME":
            "my test scenario",
            "SCENARIO_ID":
            "xyz123",
            "SCENARIO_LOCATION":
            "right here",
            "SCENARIO_THEATER":
            "PTO",
            "SCENARIO_DATE":
            "12/31/1945",
            "SCENARIO_WIDTH":
            "101",
            "ASA_ID":
            "",
            "ROAR_ID":
            "",
            "PLAYER_1":
            "russian",
            "PLAYER_1_ELR":
            "1",
            "PLAYER_1_SAN":
            "2",
            "PLAYER_1_DESCRIPTION":
            "The Army of Player 1",
            "PLAYER_2":
            "german",
            "PLAYER_2_ELR":
            "3",
            "PLAYER_2_SAN":
            "4",
            "PLAYER_2_DESCRIPTION":
            "The Army of Player 2",
            "PLAYERS_WIDTH":
            "42",
            "VICTORY_CONDITIONS":
            "just do it!",
            "VICTORY_CONDITIONS_WIDTH":
            "102",
            "SCENARIO_NOTES": [{
                "caption": "note #1",
                "width": ""
            }, {
                "caption": "note #2",
                "width": "100px"
            }],
            "SSR": ["This is an SSR.", "This is another SSR."],
            "SSR_WIDTH":
            "103",
        },
        "ob1": {
            "OB_SETUPS_1": [{
                "caption": "ob setup 1a",
                "width": ""
            }, {
                "caption": "ob setup 1b",
                "width": "200px"
            }],
            "OB_NOTES_1": [{
                "caption": "ob note 1a",
                "width": "10em"
            }, {
                "caption": "ob note 1b",
                "width": ""
            }],
            "OB_VEHICLES_1": ["a russian vehicle", "another russian vehicle"],
            "OB_VEHICLES_WIDTH_1":
            "202",
            "OB_VEHICLES_MA_NOTES_WIDTH_1":
            "203",
            "OB_ORDNANCE_1":
            ["a russian ordnance", "another russian ordnance"],
            "OB_ORDNANCE_WIDTH_1":
            "204",
            "OB_ORDNANCE_MA_NOTES_WIDTH_1":
            "205",
        },
        "ob2": {
            "OB_SETUPS_2": [{
                "caption": "ob setup 2",
                "width": ""
            }],
            "OB_NOTES_2": [{
                "caption": "ob note 2",
                "width": ""
            }],
            "OB_VEHICLES_2": ["a german vehicle"],
            "OB_VEHICLES_WIDTH_2": "302",
            "OB_VEHICLES_MA_NOTES_WIDTH_2": "303",
            "OB_ORDNANCE_2": ["a german ordnance"],
            "OB_ORDNANCE_WIDTH_2": "304",
            "OB_ORDNANCE_MA_NOTES_WIDTH_2": "305",
        },
    }
    load_scenario_params(SCENARIO_PARAMS)
    check_window_title("my test scenario (xyz123) (*)")
    check_ob_tabs("russian", "german")
    assert_scenario_params_complete(SCENARIO_PARAMS, True)

    # save the scenario and check the results
    saved_scenario = save_scenario()
    assert saved_scenario["_app_version"] == APP_VERSION
    scenario_creation_time = saved_scenario["_creation_time"]
    assert saved_scenario["_last_update_time"] == scenario_creation_time
    expected = {
        k: v
        for tab in SCENARIO_PARAMS.values() for k, v in tab.items()
    }
    mo = re.search(r"^(\d{2})/(\d{2})/(\d{4})$", expected["SCENARIO_DATE"])
    expected["SCENARIO_DATE"] = "{}-{}-{}".format(
        mo.group(3), mo.group(1), mo.group(2))  # nb: convert from ISO-8601
    saved_scenario2 = {
        k: v
        for k, v in saved_scenario.items() if not k.startswith("_")
    }
    for key in saved_scenario2:
        if re.search(r"^OB_(VEHICLES|ORDNANCE)_\d$", key):
            for vo_entry in saved_scenario2[key]:
                del vo_entry["id"]
    for key in expected:
        if re.search(r"^OB_(VEHICLES|ORDNANCE)_\d$", key):
            expected[key] = [{"name": name} for name in expected[key]]
    for player_no in (1, 2):
        for vo_type in ("OB_SETUPS", "OB_NOTES"):
            entries = expected["{}_{}".format(vo_type, player_no)]
            for i, entry in enumerate(entries):
                entry["id"] = 1 + i
        for vo_type in ("OB_VEHICLES", "OB_ORDNANCE"):
            entries = expected["{}_{}".format(vo_type, player_no)]
            for i, entry in enumerate(entries):
                entry["seq_id"] = 1 + i
    for i, entry in enumerate(expected["SCENARIO_NOTES"]):
        entry["id"] = 1 + i
    assert saved_scenario2 == expected

    # make sure that our list of scenario parameters is correct
    lhs = set(saved_scenario2.keys())
    rhs = set(itertools.chain(*ALL_SCENARIO_PARAMS.values()))
    assert lhs == rhs

    # reset the scenario and check the save results
    _ = set_stored_msg_marker("_last-info_")
    select_menu_option("new_scenario")
    wait_for(
        2, lambda: get_stored_msg("_last-info_") == "The scenario was reset.")
    check_window_title("")
    check_ob_tabs("german", "russian")
    data = save_scenario()
    assert data["_app_version"] == APP_VERSION
    assert data["_last_update_time"] == data["_creation_time"]
    assert data["_creation_time"] > scenario_creation_time
    data2 = {k: v for k, v in data.items() if not k.startswith("_") and v}
    assert data2 == {
        "SCENARIO_THEATER": "ETO",
        "PLAYER_1": "german",
        "OB_VEHICLES_MA_NOTES_WIDTH_1": "300px",
        "OB_ORDNANCE_MA_NOTES_WIDTH_1": "300px",
        "PLAYER_2": "russian",
        "OB_VEHICLES_MA_NOTES_WIDTH_2": "300px",
        "OB_ORDNANCE_MA_NOTES_WIDTH_2": "300px",
    }

    # initialize
    ssrs = find_child("#ssr-sortable")
    ob_setups1, ob_notes1 = find_child("#ob_setups-sortable_1"), find_child(
        "#ob_notes-sortable_1")
    ob_setups2, ob_notes2 = find_child("#ob_setups-sortable_2"), find_child(
        "#ob_notes-sortable_2")
    vehicles1, ordnance1 = find_child("#ob_vehicles-sortable_1"), find_child(
        "#ob_ordnance-sortable_1")
    vehicles2, ordnance2 = find_child("#ob_vehicles-sortable_2"), find_child(
        "#ob_ordnance-sortable_2")
    elems = {
        c.get_attribute("name"): c
        for elem_type in ("input", "textarea", "select")
        for c in find_children(elem_type)
    }

    # load a scenario and make sure it was loaded into the UI correctly
    load_scenario(saved_scenario)
    check_window_title("my test scenario (xyz123)")
    check_ob_tabs("russian", "german")
    for tab_id, params in SCENARIO_PARAMS.items():
        select_tab(tab_id)
        for field, val in params.items():
            if field in ("SCENARIO_NOTES", "SSR"):
                continue  # nb: these require special handling, we do it below
            if field in ("OB_SETUPS_1", "OB_SETUPS_2", "OB_NOTES_1",
                         "OB_NOTES_2"):
                continue  # nb: these require special handling, we do it below
            if field in ("OB_VEHICLES_1", "OB_ORDNANCE_1", "OB_VEHICLES_2",
                         "OB_ORDNANCE_2"):
                continue  # nb: these require special handling, we do it below
            elem = elems[field]
            if elem.tag_name == "select":
                assert Select(elem).first_selected_option.get_attribute(
                    "value") == val
            else:
                assert elem.get_attribute("value") == val
    select_tab("scenario")
    scenario_notes = [
        c.text for c in find_children("#scenario_notes-sortable li")
    ]
    assert scenario_notes == [
        sn["caption"] for sn in SCENARIO_PARAMS["scenario"]["SCENARIO_NOTES"]
    ]
    assert get_sortable_entry_text(ssrs) == SCENARIO_PARAMS["scenario"]["SSR"]
    select_tab("ob1")
    assert get_sortable_entry_text(ob_setups1) == [
        obs["caption"] for obs in SCENARIO_PARAMS["ob1"]["OB_SETUPS_1"]
    ]
    assert get_sortable_entry_text(ob_notes1) == [
        obs["caption"] for obs in SCENARIO_PARAMS["ob1"]["OB_NOTES_1"]
    ]
    # NOTE: We deleted the "id" fields above, so we rely on the legacy handling of loading by name :-/
    assert get_sortable_vo_names(
        vehicles1) == SCENARIO_PARAMS["ob1"]["OB_VEHICLES_1"]
    assert get_sortable_vo_names(
        ordnance1) == SCENARIO_PARAMS["ob1"]["OB_ORDNANCE_1"]
    select_tab("ob2")
    assert get_sortable_entry_text(ob_setups2) == [
        obs["caption"] for obs in SCENARIO_PARAMS["ob2"]["OB_SETUPS_2"]
    ]
    assert get_sortable_entry_text(ob_notes2) == [
        obs["caption"] for obs in SCENARIO_PARAMS["ob2"]["OB_NOTES_2"]
    ]
    assert get_sortable_vo_names(
        vehicles2) == SCENARIO_PARAMS["ob2"]["OB_VEHICLES_2"]
    assert get_sortable_vo_names(
        ordnance2) == SCENARIO_PARAMS["ob2"]["OB_ORDNANCE_2"]

    # save the scenario, make sure the timestamps are correct
    data = save_scenario()
    assert data["_creation_time"] == scenario_creation_time
    assert data["_last_update_time"] > scenario_creation_time
Exemple #20
0
def test_date_format(webapp, webdriver):
    """Test changing the date format."""

    # initialize
    init_webapp(webapp,
                webdriver,
                template_pack_persistence=1,
                scenario_persistence=1)

    # customize the SCENARIO template
    upload_template_pack_file(
        "scenario.j2",
        "{{SCENARIO_YEAR}}-{{SCENARIO_MONTH}}-{{SCENARIO_DAY_OF_MONTH}}",
        False)

    scenario_date = find_child("input[name='SCENARIO_DATE']")
    snippet_btn = find_child("button.generate[data-id='scenario']")

    def set_scenario_date(date_string):
        """Set the scenario date."""
        scenario_date.clear()
        scenario_date.send_keys(date_string)
        scenario_date.send_keys(Keys.TAB)
        assert scenario_date.get_attribute("value") == date_string

    def check_scenario_date(expected):
        """Check the scenario date is being interpreted correctly."""
        assert isinstance(expected, tuple) and len(expected) == 3
        assert 1 <= expected[0] <= 31 and 1 <= expected[
            1] <= 12 and 1940 <= expected[2] <= 1945
        # check the snippet
        snippet_btn.click()
        wait_for_clipboard(
            2, "{}-{}-{}".format(expected[2], expected[0], expected[1]))
        # check the save file (should always be ISO-8601 format)
        saved_scenario = save_scenario()
        assert saved_scenario["SCENARIO_DATE"] == "{:04}-{:02}-{:02}".format(
            expected[2], expected[0], expected[1])

    # check the default format (MM/DD/YYYY)
    set_scenario_date("01/02/1940")
    check_scenario_date((1, 2, 1940))
    saved_scenario = save_scenario()

    # change the date format to YYYY-MM-DD
    select_menu_option("user_settings")
    date_format_sel = Select(
        find_child(".ui-dialog.user-settings select[name='date-format']"))
    select_droplist_val(date_format_sel, "yy-mm-dd")
    click_dialog_button("OK")
    _check_cookies(webdriver, "date-format", "yy-mm-dd")

    # make sure that it took effect
    assert scenario_date.get_attribute("value") == "1940-01-02"
    check_scenario_date((1, 2, 1940))

    # clear the scenario date, set the date format to DD-MM-YYY
    set_scenario_date("")
    select_menu_option("user_settings")
    select_droplist_val(date_format_sel, "dd/mm/yy")
    click_dialog_button("OK")
    _check_cookies(webdriver, "date-format", "dd/mm/yy")

    # set the scenario date
    set_scenario_date(
        "03/04/1945")  # nb: this will be interpreted as DD/MM/YYYY
    check_scenario_date((4, 3, 1945))

    # load the scenario we saved before and check the date
    load_scenario(saved_scenario)
    check_scenario_date((1, 2, 1940))
    assert scenario_date.get_attribute("value") == "02/01/1940"
Exemple #21
0
def test_vo_images( webapp, webdriver ): #pylint: disable=too-many-statements
    """Test handling of vehicles/ordnance that have multiple images."""

    # initialize
    webapp.control_tests \
        .set_data_dir( "{REAL}" ) \
        .set_vasl_version( "random", None )
    init_webapp( webapp, webdriver, scenario_persistence=1 )

    def check_sortable2_entries( player_no, expected ):
        """Check the settings on the player's vehicles."""
        entries = find_children( "#ob_vehicles-sortable_{} li".format( player_no ) )
        for i,entry in enumerate(entries):
            # check the displayed image
            elem = find_child( "img", entry )
            assert elem.get_attribute( "src" ).endswith( expected[i][0] )
            # check the attached data
            data = webdriver.execute_script( "return $(arguments[0]).data('sortable2-data')", entry )
            assert data["vo_entry"]["id"] == expected[i][1]
            assert data["vo_image_id"] == expected[i][2]

    def check_save_scenario( player_no, expected ):
        """Check the vo_entry and vo_image_id fields are saved correctly."""
        data = save_scenario()
        assert data[ "OB_VEHICLES_{}".format(player_no) ] == expected
        return data

    # start to add a PzKw VIB
    set_player( 1, "german" )
    select_tab( "ob1" )
    add_vehicle_btn = find_child( "#ob_vehicles-add_1" )
    add_vehicle_btn.click()
    search_field = find_child( ".ui-dialog .select2-search__field" )
    search_field.send_keys( "VIB" )

    # make sure there is only 1 image available
    elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
    assert elem.get_attribute( "src" ).endswith( "/counter/2602/front" )
    vo_images = webdriver.execute_script( "return $(arguments[0]).data('vo-images')", elem )
    assert vo_images is None
    assert not find_child( "#select-vo .select2-results li input.select-vo-image" )

    # add the PzKw VIB, make sure the sortable2 entry has its data set correctly
    search_field.send_keys( Keys.RETURN )
    check_sortable2_entries( 1, [
        ( "/counter/2602/front", "ge/v:035", None )
    ] )

    # check that the vehicles are saved correctly
    check_save_scenario( 1, [
        { "id": "ge/v:035", "name": "PzKpfw VIB", "seq_id": 1 },
    ] )

    # start to add a PzKw IVH (this has multiple GPID's)
    add_vehicle_btn.click()
    search_field = find_child( ".ui-dialog .select2-search__field" )
    search_field.send_keys( "IVH" )

    # make sure multiple images are available
    elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
    assert elem.get_attribute( "src" ).endswith( "/counter/2584/front" )
    vo_images = webdriver.execute_script( "return $(arguments[0]).data('vo-images')", elem )
    assert vo_images == [ [2584,0], [2586,0], [2807,0], [2809,0] ]
    assert find_child( "#select-vo .select2-results li input.select-vo-image" )

    # add the PzKw IVH, make sure the sortable2 entry has its data set correctly
    search_field.send_keys( Keys.RETURN )
    check_sortable2_entries( 1, [
        ( "/counter/2602/front", "ge/v:035", None ),
        ( "/counter/2584/front", "ge/v:027", None ) # nb: there is no V/O image ID if it's not necessary
    ] )

    # check that the vehicles are saved correctly
    check_save_scenario( 1, [
        { "id": "ge/v:035", "name": "PzKpfw VIB", "seq_id": 1 },
        { "id": "ge/v:027", "name": "PzKpfw IVH", "seq_id": 2 }, # nb: there is no V/O image ID if it's not necessary
    ] )

    # delete the PzKw IVH
    delete_vo( "vehicles", 1, "PzKpfw IVH", webdriver )

    # add the PzKw IVH, with a different image, make sure the sortable2 entry has its data set correctly
    add_vehicle_btn.click()
    search_field = find_child( ".ui-dialog .select2-search__field" )
    search_field.send_keys( "IVH" )
    elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
    assert elem.get_attribute( "src" ).endswith( "/counter/2584/front" )
    btn = find_child( "#select-vo .select2-results li input.select-vo-image" )
    btn.click()
    images = find_children( ".ui-dialog.select-vo-image .vo-images img" )
    assert len(images) == 4
    images[2].click()
    check_sortable2_entries( 1, [
        ( "/counter/2602/front", "ge/v:035", None ),
        ( "/counter/2807/front/0", "ge/v:027", [2807,0] )
    ] )

    # check that the vehicles are saved correctly
    check_save_scenario( 1, [
        { "id": "ge/v:035", "name": "PzKpfw VIB", "seq_id": 1 },
        { "id": "ge/v:027", "image_id": "2807/0", "name": "PzKpfw IVH", "seq_id": 2 },
    ] )

    # set the British as player 2
    set_player( 2, "british" )

    # start to add a 2pdr Portee (this has multiple images for a single GPID)
    select_tab( "ob2" )
    add_vehicle_btn = find_child( "#ob_vehicles-add_2" )
    add_vehicle_btn.click()
    search_field = find_child( ".ui-dialog .select2-search__field" )
    search_field.send_keys( "2pdr" )

    # make sure multiple images are available
    elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
    assert elem.get_attribute( "src" ).endswith( "/counter/1555/front" )
    vo_images = webdriver.execute_script( "return $(arguments[0]).data('vo-images')", elem )
    assert vo_images == [ [1555,0], [1555,1] ]
    assert find_child( "#select-vo .select2-results li input.select-vo-image" )

    # add the 2pdr Portee, make sure the sortable2 entry has its data set correctly
    search_field.send_keys( Keys.RETURN )
    check_sortable2_entries( 2, [
        ( "/counter/1555/front", "br/v:115", None ) # nb: there is no V/O image ID if it's not necessary
    ] )

    # check that the vehicles are saved correctly
    check_save_scenario( 2, [
        { "id": "br/v:115", "name": "2pdr Portee", "seq_id": 1 }, # nb: there is no V/O image ID if it's not necessary
    ] )

    # delete the 2pdr Portee
    delete_vo( "vehicles", 2, "2pdr Portee", webdriver )

    # add the 2pdr Portee, with a different image, make sure the sortable2 entry has its data set correctly
    add_vehicle_btn.click()
    search_field = find_child( ".ui-dialog .select2-search__field" )
    search_field.send_keys( "2pdr" )
    elem = find_child( "#select-vo .select2-results li img[class='vasl-image']" )
    assert elem.get_attribute( "src" ).endswith( "/counter/1555/front" )
    btn = find_child( "#select-vo .select2-results li input.select-vo-image" )
    btn.click()
    images = find_children( ".ui-dialog.select-vo-image .vo-images img" )
    assert len(images) == 2
    images[1].click()
    check_sortable2_entries( 2, [
        ( "/counter/1555/front/1", "br/v:115", [1555,1] )
    ] )

    # check that the vehicles are saved correctly
    saved_scenario = check_save_scenario( 2, [
        { "id": "br/v:115", "image_id": "1555/1", "name": "2pdr Portee", "seq_id": 1 },
    ] )

    # reset the scenario
    select_menu_option( "new_scenario" )
    check_sortable2_entries( 1, [] )
    check_sortable2_entries( 2, [] )

    # load the last saved scenario, make sure the correct images are displayed
    load_scenario( saved_scenario )
    check_sortable2_entries( 1, [
        ( "/counter/2602/front", "ge/v:035", None ),
        ( "/counter/2807/front/0", "ge/v:027", ["2807",0] )
    ] )
    check_sortable2_entries( 2, [
        ( "/counter/1555/front/1", "br/v:115", ["1555",1] )
    ] )