コード例 #1
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)
コード例 #2
0
def test_zip_files(webapp, webdriver):
    """Test loading ZIP'ed template packs."""

    # initialize
    webapp.control_tests.set_vo_notes_dir("{TEST}")
    init_webapp(webapp, webdriver, template_pack_persistence=1)
    set_player(1, "german")
    set_player(2, "russian")

    # upload a template pack that contains a full set of templates
    zip_data = make_zip_from_files("full")
    _, marker = upload_template_pack_zip(zip_data, False)
    assert get_stored_msg("_last-error_") == marker

    # check that the uploaded templates are being used
    _check_snippets(webdriver,
                    lambda tid: "Customized {}.".format(tid.upper()))

    # upload only part of template pack
    _ = upload_template_pack_zip(zip_data[:int(len(zip_data) / 2)], True)
    assert get_stored_msg("_last-error_").startswith("Can't unpack the ZIP:")

    # try uploading an empty template pack
    _ = upload_template_pack_zip(b"", True)
    assert get_stored_msg("_last-error_").startswith("Can't unpack the ZIP:")
コード例 #3
0
def test_individual_files(webapp, webdriver):
    """Test loading individual template files."""

    # initialize
    webapp.control_tests.set_vo_notes_dir("{TEST}")
    init_webapp(webapp, webdriver, template_pack_persistence=1)
    set_player(1, "german")
    set_player(2, "russian")

    # try uploading a customized version of each template
    def test_template(template_id, orig_template_id):
        """Test uploading a customized version of the template."""
        # upload a new template
        _ = upload_template_pack_file(template_id + ".j2", "UPLOADED TEMPLATE",
                                      False)
        # make sure generating a snippet returns the new version
        _ = _generate_snippet(webdriver, template_id, orig_template_id)
        wait_for_clipboard(2, "UPLOADED TEMPLATE")

    for_each_template(test_template)

    # try uploading a template with an incorrect filename extension
    _ = upload_template_pack_file("filename.xyz", "UPLOADED TEMPLATE", True)
    assert "Invalid template extension" in get_stored_msg("_last-error_")

    # try uploading a template with an unknown filename
    _ = upload_template_pack_file("unknown.j2", "UPLOADED TEMPLATE", True)
    assert "Invalid template filename" in get_stored_msg("_last-error_")
コード例 #4
0
    def do_test():  #pylint: disable=missing-docstring

        # initialize
        init_webapp(webapp, webdriver, vlog_persistence=1, lfa_persistence=1)

        # analyze the log file
        _analyze_vlogs("custom-labels.vlog")

        # download the data
        marker = set_stored_msg_marker("_lfa-download_")
        find_child("#lfa button.download").click()
        wait_for(2, lambda: get_stored_msg("_lfa-download_") != marker)
        data = get_stored_msg("_lfa-download_")

        # check the results
        data = data.split("\n")
        rows = list(csv.reader(data, quoting=csv.QUOTE_NONNUMERIC))
        assert rows == [[
            "Log file", "Phase", "Player", "Type", "Die 1", "Die 2"
        ], ["custom-labels.vlog", "", "test", "Other", 5, 3],
                        ["", "", "test", "Other", 3, ""],
                        ["", "Custom Label 1", "test", "Other", 6, 6],
                        ["", "", "test", "RS", 6, ""],
                        ["", "Axis 1 PFPh", "test", "Other", 4, 4],
                        ["", "", "test", "RS", 6, ""],
                        ["", "Custom label 2", "test", "Other", 2, 1],
                        ["", "", "test", "RS", 1, ""]]
コード例 #5
0
    def do_test():  #pylint: disable=missing-docstring

        # initialize
        init_webapp(webapp, webdriver, vlog_persistence=1, lfa_persistence=1)

        # analyze the log file
        _analyze_vlogs("download-test.vlog")

        # download the data
        marker = set_stored_msg_marker("_lfa-download_")
        find_child("#lfa button.download").click()
        wait_for(2, lambda: get_stored_msg("_lfa-download_") != marker)
        data = get_stored_msg("_lfa-download_")

        # check the results
        data = data.split("\n")
        rows = list(csv.reader(data, quoting=csv.QUOTE_NONNUMERIC))
        assert rows == [[
            "Log file", "Phase", "Player", "Type", "Die 1", "Die 2"
        ], ["download-test.vlog", "", 'Joey "The Lips" Blow', "IFT", 4, 1],
                        ["", "", 'Joey "The Lips" Blow', "IFT", 2, 5],
                        ["", "", 'Joey "The Lips" Blow', "RS", 2, ""],
                        ["", "UN 1 PFPh", "\u65e5\u672c Guy", "IFT", 4, 6],
                        ["", "", "\u65e5\u672c Guy", "IFT", 2, 6],
                        ["", "", "\u65e5\u672c Guy", "RS", 3, ""],
                        ["", "UN 1 MPh", 'Joey "The Lips" Blow', "IFT", 2, 6],
                        ["", "", 'Joey "The Lips" Blow', "IFT", 2, 3],
                        ["", "", 'Joey "The Lips" Blow', "RS", 3, ""]]
コード例 #6
0
def _test_snippet(btn, params, expected, expected2):
    """Do a single test."""

    # set the template parameters and generate the snippet
    set_template_params(params)
    marker = set_stored_msg_marker("_last-warning_")
    btn.click()

    def reformat(clipboard):  #pylint: disable=missing-docstring
        lines = [l.strip() for l in clipboard.split("\n")]
        return " | ".join(l for l in lines if l)

    wait_for_clipboard(2, expected, transform=reformat)

    # check warnings for mandatory parameters
    last_warning = get_stored_msg("_last-warning_")
    if isinstance(expected2, list):
        # check for mandatory parameters
        param_names = ["scenario name", "scenario location", "scenario date"]
        for pname in param_names:
            if pname in expected2:
                assert pname in last_warning
            else:
                assert pname not in last_warning
    elif isinstance(expected2, str):
        # check for a specific error message
        assert expected2 == last_warning
    else:
        # make sure there was no warning message
        assert expected2 is None
        assert last_warning == marker
コード例 #7
0
    def do_check_snippets( btn, date, expected, warning ):
        """Check that snippets are being generated correctly."""

        # change the scenario date, check that the button is displayed correctly
        set_scenario_date( "{:02}/01/{:04}".format( date[1], date[0] ) )
        select_tab( "ob1" )
        classes = btn.get_attribute( "class" )
        classes = classes.split() if classes else []
        if warning:
            assert "inactive" in classes
        else:
            assert "inactive" not in classes

        # test snippet generation
        marker = set_stored_msg_marker( "_last-warning_" )
        btn.click()
        wait_for_clipboard( 2, expected )

        # check if a warning was issued
        last_warning = get_stored_msg( "_last-warning_" )
        if warning:
            assert "are only available" in last_warning
            expected_image_url = "snippet-disabled.png"
        else:
            assert last_warning == marker
            expected_image_url = "snippet.png"
        wait_for( 2,
            lambda: expected_image_url in find_child( "img", btn ).get_attribute( "src" )
        )
コード例 #8
0
def test_nationality_data(webapp, webdriver):
    """Test a template pack with nationality data."""

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

    # select the British as player 1
    player1_sel = set_player(1, "british")
    tab_ob1 = find_child("a[href='#tabs-ob1']")
    assert tab_ob1.text.strip() == "British OB"
    # FUDGE!  player1_sel.first_selected_option.text doesn't contain the right value
    # if we're using jQuery selectmenu's :-/
    assert get_player_nat(1) == "british"
    droplist_vals = get_droplist_vals_index(player1_sel)
    assert droplist_vals["british"] == "British"

    # upload a template pack that contains nationality data
    zip_data = make_zip_from_files("with-nationality-data")
    _, marker = upload_template_pack_zip(zip_data, False)
    assert get_stored_msg("_last-error_") == marker

    # check that the UI was updated correctly
    assert tab_ob1.text.strip() == "Poms! OB"
    assert get_player_nat(1) == "british"
    droplist_vals2 = get_droplist_vals_index(player1_sel)
    assert droplist_vals2["british"] == "Poms!"

    # check that there is a new Korean player
    del droplist_vals2["korean"]
    droplist_vals2 = {
        k: "British" if v == "Poms!" else v
        for k, v in droplist_vals2.items()
    }
    assert droplist_vals2 == droplist_vals
コード例 #9
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")
コード例 #10
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.")
コード例 #11
0
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
コード例 #12
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
コード例 #13
0
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)
コード例 #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)
コード例 #15
0
def test_unknown_vo(webapp, webdriver):
    """Test detection of unknown vehicles/ordnance."""

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

    # load a scenario that has unknown vehicles/ordnance
    SCENARIO_PARAMS = {
        "PLAYER_1":
        "german",
        "OB_VEHICLES_1": [
            {
                "name": "unknown vehicle 1a"
            },
            {
                "name": "unknown vehicle 1b"
            },
        ],
        "OB_ORDNANCE_1": [
            {
                "name": "unknown ordnance 1a"
            },
            {
                "name": "unknown ordnance 1b"
            },
        ],
        "PLAYER_2":
        "russian",
        "OB_VEHICLES_2": [{
            "name": "unknown vehicle 2"
        }],
        "OB_ORDNANCE_2": [{
            "name": "unknown ordnance 2"
        }],
    }
    _ = set_stored_msg_marker("_last-warning_")
    load_scenario(SCENARIO_PARAMS)
    last_warning = get_stored_msg("_last-warning_")
    assert last_warning.startswith("Unknown vehicles/ordnance:")
    for key, vals in SCENARIO_PARAMS.items():
        if not key.startswith(("OB_VEHICLES_", "OB_ORDNANCE_")):
            continue
        assert all(v["name"] in last_warning for v in vals)
コード例 #16
0
def test_invalid_vo_image_ids( webapp, webdriver ):
    """Test loading scenarios that contain invalid V/O image ID's."""

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

    # test each save file
    dname = os.path.join( os.path.split(__file__)[0], "fixtures/invalid-vo-image-ids" )
    for root,_,fnames in os.walk(dname):
        for fname in fnames:
            fname = os.path.join( root, fname )
            if os.path.splitext( fname )[1] != ".json":
                continue

            # load the next scenario, make sure a warning was issued for the V/O image ID
            with open( fname, "r", encoding="utf-8" ) as fp:
                data = json.load( fp )
            set_stored_msg_marker( "_last-warning_" )
            load_scenario( data )
            last_warning = get_stored_msg( "_last-warning_" )
            assert "Invalid V/O image ID" in last_warning
コード例 #17
0
 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:")
コード例 #18
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
コード例 #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