import contextlib import io import json import os import tempfile import unittest from datetime import datetime from pathlib import Path from unittest.mock import patch import lvpwb as app class RosterTests(unittest.TestCase): def test_rosters_load_from_uniform_json_payloads(self): self.assertEqual(len(app.load_roster("sd1")), 36) self.assertEqual(len(app.load_roster("sd2")), 65) self.assertEqual(len(app.load_roster("sd3")), 43) for game_id, game in app.GAMES.items(): payload = json.loads((app.ROOT / game["roster"]).read_text(encoding="utf-8")) self.assertEqual(set(payload), {"game_id", "game", "wrestlers"}) self.assertEqual(payload["game_id"], game_id) self.assertEqual(payload["game"], game["title"]) class MarkdownTests(unittest.TestCase): def make_card(self, path: Path) -> dict: def profile_fields(match_type: str) -> dict: definition = app.match_type_definition("sd2", match_type) return {"base_rating": definition["base_rating"], "valid_outcomes": list(definition["outcomes"])} return { "game_id": "sd2", "show": 'The "Big" Show', "date": "2026-10-25", "path": str(path), "matches": [ {"title": "Opener", "type": "Singles", "sides": [["The Rock"], ["Kane"]], "winner_side": 0, "finish": "pinfall", "promo": None, "image": "", "ranking_division": None, "title_defense": False, "rivalry_payoff": False, "match_time_minutes": None, "near_falls": 0, "finishers": 0, "rating": None, **profile_fields("Singles")}, {"title": "", "type": "Tag Team", "sides": [["Edge", "Christian"], ["The Hardy Boyz", "Tazz"]], "winner_side": None, "finish": "", "promo": None, "image": "", "ranking_division": None, "title_defense": False, "rivalry_payoff": False, "match_time_minutes": None, "near_falls": 0, "finishers": 0, "rating": None, **profile_fields("Tag Team")}, {"title": "", "type": "Triple Threat", "sides": [], "winner_side": None, "finish": "", "promo": None, "image": "", "ranking_division": None, "title_defense": False, "rivalry_payoff": False, "match_time_minutes": None, "near_falls": 0, "finishers": 0, "rating": None, **profile_fields("Triple Threat")}, ], } def test_markdown_round_trip_including_incomplete_match(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "card.md" card = self.make_card(path) app.save_card(card) loaded = app.load_card(path) self.assertEqual(loaded["show"], card["show"]) self.assertEqual(loaded["game_id"], card["game_id"]) self.assertEqual(loaded["matches"], card["matches"]) def test_load_pads_short_card_to_three_matches(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "short.md" card = self.make_card(path) card["matches"] = card["matches"][:1] app.save_card(card) loaded = app.load_card(path) self.assertEqual(len(loaded["matches"]), 3) self.assertEqual(loaded["matches"][1]["sides"], []) def test_render_shows_match_names_and_recorded_winner(self): card = self.make_card(Path("card.md")) with patch.object(app, "_render_image", return_value=([], 0)): rendered = app.render_card(card) flashing = app.render_card(card, flash=True) self.assertIn("Match 1 — Opener", rendered) self.assertIn("The Rock", rendered) self.assertIn("WIN!", flashing) self.assertIn("UP NEXT", rendered) main_event_position = rendered.index("Match 3") second_match_position = rendered.index("Match 2") opener_position = rendered.index("Match 1 — Opener") self.assertLess(main_event_position, second_match_position) self.assertLess(second_match_position, opener_position) class RatingTests(unittest.TestCase): def test_fixed_match_type_bases(self): self.assertEqual(app.base_match_rating("Singles"), 4.5) self.assertEqual(app.base_match_rating("TLC"), 7.5) self.assertEqual(app.base_match_rating("Hell in a Cell"), 9.5) def test_time_near_falls_and_finishers_drive_match_rating(self): match = {"type": "Singles", "match_time_minutes": 12.9, "near_falls": 2, "finishers": 3, "title_defense": False} self.assertEqual(app.calculate_match_rating(match), 7.6) match["title_defense"] = True self.assertEqual(app.calculate_match_rating(match), 11.4) def test_combined_live_bonus_is_capped_before_title_multiplier(self): match = {"type": "Singles", "match_time_minutes": 200, "near_falls": 100, "finishers": 100, "title_defense": True} self.assertEqual(app.calculate_match_rating(match), 21.8) def test_match_duration_and_counts_validate(self): self.assertEqual(app.parse_match_minutes("12.5"), 12.5) self.assertEqual(app.parse_match_minutes("0"), 0) for invalid in ("-1", "nan", "inf", "abc"): self.assertIsNone(app.parse_match_minutes(invalid)) self.assertEqual(app.parse_action_count("3"), 3) self.assertIsNone(app.parse_action_count("-1")) self.assertIsNone(app.parse_action_count("2.5")) def test_ppv_uses_weighted_three_match_score_only_when_all_are_rated(self): card = {"show": "WWF PPV", "matches": [{"rating": value} for value in (10.0, 20.0, 30.0)]} self.assertEqual(app.calculate_ppv_rating(card), 23.0) card["matches"][1]["rating"] = None self.assertIsNone(app.calculate_ppv_rating(card)) card["show"] = "Monday Night Raw" self.assertIsNone(app.calculate_ppv_rating(card)) def test_match_and_ppv_ratings_round_trip_in_markdown(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "ppv.md" card = MarkdownTests().make_card(path) card["show"] = "WWF PPV" for match, rating in zip(card["matches"], (10.0, 20.0, 30.0)): match.update(match_time_minutes=12.5, near_falls=2, finishers=1, rating=rating) app.save_card(card) loaded = app.load_card(path) self.assertEqual(loaded["matches"][1]["match_time_minutes"], 12.5) self.assertEqual(loaded["matches"][1]["near_falls"], 2) self.assertEqual(loaded["matches"][1]["finishers"], 1) self.assertEqual(loaded["matches"][1]["rating"], 20.0) self.assertEqual(loaded["ppv_rating"], 23.0) with patch.object(app, "_render_image", return_value=([], 0)): rendered = app.render_card(loaded) self.assertIn("WWF PPV Rating: 23.0%", rendered) def test_historical_brand_names_use_thursday_and_generic_ppv(self): self.assertEqual(app.show_name_for_brand("SmackDown"), "Thursday Night SmackDown!") self.assertEqual(app.show_name_for_brand("PPV"), "WWF PPV") def test_result_prompt_records_duration_near_falls_finishers_and_rating(self): with tempfile.TemporaryDirectory() as folder: card = MarkdownTests().make_card(Path(folder) / "card.md") answers = ["1", "1", "1", "1", "12.5", "3", "2"] with patch("builtins.input", side_effect=answers), \ contextlib.redirect_stdout(io.StringIO()): app.record_result(card) match = card["matches"][1] self.assertEqual(match["winner_side"], 0) self.assertEqual(match["match_time_minutes"], 12.5) self.assertEqual(match["near_falls"], 3) self.assertEqual(match["finishers"], 2) self.assertEqual(match["rating"], 7.3) class MatchLabelTests(unittest.TestCase): def test_ranking_division_alone_is_labeled_non_title(self): match = {"title": "", "type": "Singles", "sides": [["The Undertaker"], ["Viscera"]], "winner_side": None, "ranking_division": "European", "title_defense": False} rendered = "\n".join(app.match_lines(match, 1, 100)) self.assertIn("NON-TITLE MATCH", rendered) self.assertNotIn("EUROPEAN", rendered) def test_only_explicit_title_match_displays_championship(self): match = {"title": "European Championship", "type": "Singles", "sides": [["The Undertaker"], ["Viscera"]], "winner_side": None, "ranking_division": "European", "title_defense": True} rendered = "\n".join(app.match_lines(match, 1, 100)) self.assertIn("EUROPEAN TITLE MATCH", rendered) self.assertNotIn("NON-TITLE MATCH", rendered) class PromoVoiceTests(unittest.TestCase): def test_rock_austin_and_triple_h_have_signature_voices(self): with patch.object(app.random, "choice", side_effect=lambda options: options[0]): rock = app.promo_line_for("The Rock", "Mankind", "openers") austin = app.promo_line_for("Stone Cold Steve Austin", "The Rock", "challenges") triple_h = app.promo_line_for("Triple H", "Kane", "openers") self.assertIn("jabroni", rock.lower()) with patch.object(app.random, "choice", side_effect=lambda options: options[0]): rock_with_opponent = app.promo_line_for("The Rock", "Jeff Hardy", "openers") self.assertIn("jabroni Jeff Hardy—", rock_with_opponent) self.assertIn("bottom line", austin.lower()) self.assertIn("I am the Game", triple_h) def test_mankind_has_persecuted_little_guy_twisted_idiom_and_pain(self): with patch.object(app.random, "choice", side_effect=lambda options: options[0]): line = app.promo_line_for("Mick Foley", "The Rock", "challenges") self.assertIn("little guy", line) self.assertIn("worm gets the bird", line) self.assertIn("bricks", line) def test_unprofiled_wrestler_uses_generic_fallback(self): with patch.object(app.random, "choice", side_effect=lambda options: options[0]): line = app.promo_line_for("Debra", "The Rock", "openers") self.assertIn(app.PROMO_OPENERS[0], line) def test_every_profile_formats_for_each_promo_turn(self): with patch.object(app.random, "choice", side_effect=lambda options: options[0]): for alias in app.PROMO_PROFILE_ALIASES: for moment in ("openers", "challenges", "retorts"): line = app.promo_line_for(alias, "Jeff Hardy", moment) self.assertTrue(line) self.assertNotIn(":", line, f"Unexpected speaker-like colon for {alias}: {line}") for profile_key, profile in app.PROMO_PROFILES.items(): for moment in ("openers", "challenges", "retorts"): for template in profile[moment]: line = template.format(opponent="Jeff Hardy", wrestler="Test Wrestler") self.assertNotIn(":", line, f"Unexpected colon in {profile_key}: {line}") for bank in (app.PROMO_OPENERS, app.PROMO_CHALLENGES, app.PROMO_RETORTS): for line in bank: self.assertNotIn(":", line) class MatchOutcomeTests(unittest.TestCase): def test_match_types_only_offer_legal_win_conditions(self): self.assertEqual(app.allowed_match_outcomes("Ladder"), ("Item retrieval",)) self.assertEqual(app.allowed_match_outcomes("TLC"), ("Item retrieval",)) self.assertEqual(app.allowed_match_outcomes("Table"), ("Table break",)) self.assertEqual(app.allowed_match_outcomes("Battle Royal"), ("Last wrestler standing",)) self.assertEqual(app.allowed_match_outcomes("Cage"), ("Escape the cage",)) self.assertEqual(app.allowed_match_outcomes("Singles"), ("Pinfall", "Submission")) def test_auto_sim_uses_only_valid_finish_and_randomizes_match_stats(self): card = MarkdownTests().make_card(Path("card.md")) card["matches"][1].update(type="Ladder", sides=[["Edge"], ["Kane"]]) with patch.object(app.random, "choices", return_value=[0]), \ patch.object(app.random, "choice", side_effect=lambda options: options[0]), \ patch.object(app.random, "randint", side_effect=lambda low, high: low): result = app.simulate_match_result(card, 1) self.assertEqual(result["finish"], "Item retrieval") self.assertIn(result["winner_side"], (0, 1)) self.assertIsNotNone(result["match_time_minutes"]) self.assertIsNotNone(result["rating"]) def test_auto_simulate_all_completes_and_persists_pending_matches(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "card.md" card = MarkdownTests().make_card(path) card["matches"][1]["type"] = "Ladder" with patch.object(app.random, "choices", return_value=[1]), \ patch.object(app.random, "choice", side_effect=lambda options: options[0]), \ patch.object(app.random, "randint", side_effect=lambda low, _high: low), \ contextlib.redirect_stdout(io.StringIO()): app.simulate_pending_matches(card) match = app.load_card(path)["matches"][1] self.assertEqual(match["winner_side"], 1) self.assertEqual(match["finish"], "Item retrieval") self.assertIsNotNone(match["rating"]) def test_season_simulation_weights_higher_ranked_sides_but_one_off_is_even(self): division = "European" season = app.new_season_state("Sim Test", "sd1", season_id="sim-test") for wrestler, wins, losses in (("Kane", 5, 0), ("Mankind", 0, 5)): season["rankings"][division][app.side_identity([wrestler])] = { "members": [wrestler], "wins": wins, "losses": losses, "recent": ["W"] * wins + ["L"] * losses, } match = {"ranking_division": division, "title_defense": False, "sides": [["Kane"], ["Mankind"]]} weights = app.season_simulation_weights(season, match) self.assertGreater(weights[0], weights[1]) self.assertEqual(app.season_simulation_weights(None, match), [1.0, 1.0]) def test_ranked_non_title_bouts_are_not_labeled_contender_matches(self): season = app.new_season_state("Test Season", "sd1", season_id="rank-label-test") candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "Raw", set()) ranked = [candidate["match"] for candidate in candidates if candidate["kind"] == "ranked"] self.assertTrue(ranked) self.assertTrue(all(match["title"] == "" for match in ranked)) self.assertTrue(all(not match["title_defense"] for match in ranked)) def test_season_card_exit_label_changes_after_all_matches_complete(self): card = MarkdownTests().make_card(Path("season.md")) card["season_id"] = "season-test" self.assertEqual(app.season_card_exit_label(card), "Return to Season Menu") for match in card["matches"]: match["winner_side"] = 0 self.assertEqual(app.season_card_exit_label(card), "Advance to Next Show") card.pop("season_id") self.assertEqual(app.season_card_exit_label(card), "Back to main menu") class YesNoPromptTests(unittest.TestCase): def test_yes_no_prompt_accepts_short_and_long_answers_case_insensitively(self): with patch("builtins.input", side_effect=["YES", "n"]): self.assertTrue(app.ask_yes_no("Title defense?")) self.assertFalse(app.ask_yes_no("Title defense?")) def test_yes_no_prompt_reprompts_invalid_input_and_keeps_default_on_blank(self): with contextlib.redirect_stdout(io.StringIO()), \ patch("builtins.input", side_effect=["maybe", "y", ""]): self.assertTrue(app.ask_yes_no("Title defense?", default=False)) self.assertTrue(app.ask_yes_no("Title defense?", default=True)) class LocationTests(unittest.TestCase): def ask_location(self, answers: list[str]) -> tuple[str, int]: with contextlib.redirect_stdout(io.StringIO()), \ patch("builtins.input", side_effect=answers): return app.ask_location() def test_random_location_uses_known_city_range(self): city, attendance = app.random_location() self.assertIn(city, app.CITY_ATTENDANCE_RANGES) low, high = app.CITY_ATTENDANCE_RANGES[city] self.assertGreaterEqual(attendance, low) self.assertLessEqual(attendance, high) def test_manual_city_and_attendance_are_used_as_entered(self): self.assertEqual(self.ask_location(["Boston, MA", "15,450"]), ("Boston, MA", 15450)) def test_random_city_prompt_skips_attendance_question(self): with contextlib.redirect_stdout(io.StringIO()), \ patch("builtins.input", side_effect=["r"]): city, attendance = app.ask_location() self.assertIn(city, app.CITY_ATTENDANCE_RANGES) low, high = app.CITY_ATTENDANCE_RANGES[city] self.assertTrue(low <= attendance <= high) def test_random_attendance_follows_entered_city_range(self): city, attendance = self.ask_location(["Osaka, Japan", "r"]) self.assertEqual(city, "Osaka, Japan") low, high = app.CITY_ATTENDANCE_RANGES["Osaka, Japan"] self.assertTrue(low <= attendance <= high) def test_invalid_attendance_reprompts(self): self.assertEqual(self.ask_location(["Tokyo, Japan", "abc", "0", "18000"]), ("Tokyo, Japan", 18000)) def test_unknown_city_random_attendance_uses_default_range(self): low, high = app.DEFAULT_ATTENDANCE_RANGE for _ in range(30): self.assertTrue(low <= app.random_attendance("Springfield, OR") <= high) def test_markdown_preserves_city_and_attendance(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "card.md" card = MarkdownTests().make_card(path) card["city"] = "Toronto, ON" card["attendance"] = 17500 app.save_card(card) loaded = app.load_card(path) self.assertEqual(loaded["city"], "Toronto, ON") self.assertEqual(loaded["attendance"], 17500) def test_legacy_card_without_location_still_loads(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "legacy.md" card = MarkdownTests().make_card(path) app.save_card(card) text = path.read_text(encoding="utf-8") path.write_text("\n".join( line for line in text.splitlines() if not line.startswith(("- City:", "- Attendance:")) ) + "\n", encoding="utf-8") loaded = app.load_card(path) self.assertEqual(loaded["city"], "") self.assertIsNone(loaded["attendance"]) def test_header_and_listing_show_location_next_to_show_name(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "card.md" card = MarkdownTests().make_card(path) card["city"] = "Osaka, Japan" card["attendance"] = 12000 app.save_card(card) with patch.object(app, "_render_image", return_value=([], 0)): header = "\n".join(app.header_lines(card, 100)) self.assertIn('The "Big" Show', header) self.assertIn("Osaka, Japan", header) self.assertIn("12,000 fans", header) summary = app.card_summary(path) self.assertIn('The "Big" Show', summary) self.assertIn("Osaka, Japan", summary) self.assertIn("12,000 fans", summary) class ScreenshotTests(unittest.TestCase): def make_screenshots(self, folder: Path) -> dict[str, Path]: names = { "old": "WWF SmackDown 2026-01-01-10-00-00.png", "newest": "WWF SmackDown 2026-10-26-21-30-00.png", "newer": "WWF SmackDown 2026-10-26-09-15-00.png", "dated": "No stamp in this name.png", } paths = {key: folder / value for key, value in names.items()} for path in paths.values(): path.write_bytes(b"png") return paths def test_parse_screenshot_stamp_reads_filename(self): stamp = app.parse_screenshot_stamp("WWF SmackDown 2026-10-26-21-30-00.png") self.assertEqual(stamp, datetime(2026, 10, 26, 21, 30, 0).astimezone()) self.assertIsNone(app.parse_screenshot_stamp("No stamp in this name.png")) def test_session_filter_keeps_only_shots_after_since_with_mtime_fallback(self): with tempfile.TemporaryDirectory() as folder: directory = Path(folder) paths = self.make_screenshots(directory) fall_back = datetime(2026, 10, 26, 8, 0, 0).astimezone() os.utime(paths["dated"], (fall_back.timestamp(), fall_back.timestamp())) since = datetime(2026, 10, 26, 7, 0, 0).astimezone() found = app.session_screenshots(since=since, directory=directory) self.assertEqual(found, [paths["newest"], paths["newer"], paths["dated"]]) self.assertNotIn(paths["old"], app.session_screenshots( since=datetime(2026, 10, 26, 9, 0, 0).astimezone(), directory=directory)) def test_missing_screenshot_directory_lists_nothing(self): self.assertEqual(app.session_screenshots(directory=Path("/nonexistent-dir")), []) def test_builtin_games_use_their_emulator_screenshot_directories(self): self.assertEqual(app.screenshot_directory_for_game("sd1"), app.DUCKSTATION_SCREENSHOT_DIR) self.assertEqual(app.screenshot_directory_for_game("sd2"), app.DUCKSTATION_SCREENSHOT_DIR) self.assertEqual(app.screenshot_directory_for_game("sd3"), app.PCSX2_SCREENSHOT_DIR) self.assertTrue(app.game_has_screenshots("sd3")) def test_attach_uses_the_games_screenshot_directory(self): with tempfile.TemporaryDirectory() as folder: card = MarkdownTests().make_card(Path(folder) / "card.md") card["game_id"] = "sd3" with patch.object(app, "choose_match_index", return_value=0), \ patch.object(app, "choose_screenshot", return_value=Path(folder) / "shot.png") as choose, \ contextlib.redirect_stdout(io.StringIO()): app.attach_match_image(card) choose.assert_called_once_with(app.PCSX2_SCREENSHOT_DIR) self.assertEqual(card["matches"][0]["image"], str(Path(folder) / "shot.png")) def test_attach_flow_prompts_yes_and_stores_chosen_image(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "card.md" card = MarkdownTests().make_card(path) chosen = Path(folder) / "WWF SmackDown 2026-10-26-21-30-00.png" chosen.write_bytes(b"png") with contextlib.redirect_stdout(io.StringIO()), \ patch("builtins.input", side_effect=["2", "1"]), \ patch.object(app, "session_screenshots", return_value=[chosen]), \ patch.object(app, "DUCKSTATION_SCREENSHOT_DIR", Path(folder)): app.attach_match_image(card) self.assertEqual(card["matches"][1]["image"], str(chosen)) self.assertIn(f'- Image: "{chosen}"', path.read_text(encoding="utf-8")) self.assertEqual(app.load_card(path)["matches"][1]["image"], str(chosen)) def test_attach_declines_when_game_has_no_screenshot_directory(self): with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "card.md" card = MarkdownTests().make_card(path) card["game_id"] = "unknown-other-game" before = json.dumps(card["matches"]) with contextlib.redirect_stdout(io.StringIO()), \ patch("builtins.input", side_effect=RuntimeError("unexpected prompt")): app.attach_match_image(card) self.assertEqual(json.dumps(card["matches"]), before) class RandomShowTests(unittest.TestCase): def test_randomized_card_uses_selected_games_roster_without_repeats(self): with tempfile.TemporaryDirectory() as folder: with patch.object(app, "CARD_DIR", Path(folder)), \ patch.object(app, "choose_game", return_value="sd2"), \ patch.object(app.random, "seed", 42), \ contextlib.redirect_stdout(io.StringIO()): card = app.create_random_card() roster = set(app.load_roster("sd2")) self.assertEqual(len(card["matches"]), 3) self.assertIn(card["show"], {"Monday Night Raw", "Thursday Night SmackDown!", "WWF PPV"}) for match in card["matches"]: wrestlers = [name for side in match["sides"] for name in side] self.assertTrue(set(wrestlers) <= roster) self.assertEqual(len(wrestlers), len(set(wrestlers))) self.assertIsNone(match["winner_side"]) class SelectionPromptTests(unittest.TestCase): def test_numeric_choice_shows_exact_labels_and_repeats_them_on_invalid_input(self): with patch("builtins.input", side_effect=["3", "1"]), \ contextlib.redirect_stdout(io.StringIO()) as output: selected = app.ask_choice("Winning method", ["Pinfall", "Submission"]) self.assertEqual(selected, 0) self.assertEqual(output.getvalue().count("1. Pinfall"), 2) self.assertIn("2. Submission", output.getvalue()) self.assertIn("Enter a number from 1 to 2.", output.getvalue()) def test_manual_wrestler_selection_is_alphabetical(self): roster = app.load_roster("sd2") ordered = sorted(roster, key=str.casefold) with patch("builtins.input", side_effect=["1", "2"]), \ contextlib.redirect_stdout(io.StringIO()) as output: sides = app.choose_sides((1, 1), roster) self.assertEqual(sides, [[ordered[0]], [ordered[1]]]) listing = output.getvalue() self.assertLess(listing.index(ordered[0]), listing.index(ordered[1])) def test_result_mode_displays_manual_and_auto_sim_options(self): with tempfile.TemporaryDirectory() as folder: card = MarkdownTests().make_card(Path(folder) / "card.md") with patch("builtins.input", side_effect=["1", "2"]), \ patch.object(app.random, "choices", return_value=[0]), \ patch.object(app.random, "choice", side_effect=lambda options: options[0]), \ patch.object(app.random, "randint", side_effect=lambda low, _high: low), \ contextlib.redirect_stdout(io.StringIO()) as output: app.record_result(card) self.assertIn("1. Enter result manually", output.getvalue()) self.assertIn("2. Auto-simulate result", output.getvalue()) self.assertIsNotNone(card["matches"][1]["rating"]) class SeasonModeTests(unittest.TestCase): def season(self, **kwargs) -> dict: return app.new_season_state( kwargs.get("name", "Test Season"), "sd1", kwargs.get("champions", {}), kwargs.get("season_id", "test-season"), ) def test_initial_single_title_champion_options_match_displayed_indices(self): with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", return_value=0), \ patch.object(app, "choose_roster_member", return_value="Al Snow") as choose: self.assertEqual(app.choose_initial_champion("European", "sd1", set()), ["Al Snow"]) choose.assert_called_once() with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", return_value=1), \ patch.object(app.random, "choice", return_value="The Rock"), \ patch.object(app, "choose_roster_member") as choose: self.assertEqual(app.choose_initial_champion("European", "sd1", set()), ["The Rock"]) choose.assert_not_called() with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", return_value=2), \ patch.object(app, "choose_roster_member") as choose: self.assertIsNone(app.choose_initial_champion("European", "sd1", set())) choose.assert_not_called() def test_initial_tag_team_champion_options_match_displayed_indices(self): with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", return_value=0), \ patch.object(app, "choose_roster_member", side_effect=["Al Snow", "The Rock"]): self.assertEqual(app.choose_initial_champion("Tag Team", "sd1", set()), ["Al Snow", "The Rock"]) with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", return_value=1), \ patch.object(app.random, "sample", return_value=["Al Snow", "The Rock"]), \ patch.object(app, "choose_roster_member") as choose: self.assertEqual(app.choose_initial_champion("Tag Team", "sd1", set()), ["Al Snow", "The Rock"]) choose.assert_not_called() with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", return_value=2), \ patch.object(app, "choose_roster_member") as choose: self.assertIsNone(app.choose_initial_champion("Tag Team", "sd1", set())) choose.assert_not_called() def test_raw_smackdown_ppv_cycle_repeats(self): self.assertEqual([app.season_show_brand(n) for n in range(1, 7)], ["Raw", "SmackDown", "PPV", "Raw", "SmackDown", "PPV"]) def test_wins_and_losses_update_division_rankings(self): season = self.season() match = {"ranking_division": "Intercontinental", "winner_side": 0, "sides": [["The Rock"], ["Kane"]], "title_defense": False, "rivalry_payoff": False} app.record_season_match(season, {"season_show": "Raw"}, match, 0) entries = app.season_ranking_entries(season, "Intercontinental") self.assertEqual(entries[0]["members"], ["The Rock"]) self.assertEqual(entries[0]["wins"], 1) self.assertEqual(entries[1]["losses"], 1) def test_title_defenses_increment_and_title_changes_reset_current_reign(self): division = "World Wrestling Federation" season = self.season(champions={division: ["The Rock"]}) successful = {"ranking_division": division, "title_defense": True, "rivalry_payoff": False, "sides": [["The Rock"], ["Kane"]], "winner_side": 0} app.record_season_match(season, {"season_show": "Raw"}, successful, 0) belt = season["champions"][division] self.assertEqual(belt["successful_defenses"], 1) self.assertEqual(belt["defense_history"][app.side_identity(["The Rock"])] ["successful_defenses"], 1) new_champion = dict(successful, winner_side=1) app.record_season_match(season, {"season_show": "PPV"}, new_champion, 1) self.assertEqual(belt["holder"], ["Kane"]) self.assertEqual(belt["successful_defenses"], 0) self.assertEqual(belt["defense_history"][app.side_identity(["The Rock"])] ["successful_defenses"], 1) def test_tag_team_champion_is_ranked_and_defenses_count_for_team(self): division = "Tag Team" team = ["Edge", "Christian"] season = self.season(champions={division: team}) match = {"ranking_division": division, "title_defense": True, "rivalry_payoff": False, "sides": [team, ["The Hardy Boyz", "Tazz"]], "winner_side": 0} app.record_season_match(season, {"season_show": "SmackDown"}, match, 0) self.assertEqual(season["champions"][division]["successful_defenses"], 1) self.assertEqual(app.season_ranking_entries(season, division)[0]["members"], team) def test_others_pool_is_unranked_and_excludes_title_division_participants(self): season = self.season(champions={"European": ["Kane"]}) season["rankings"]["European"][app.side_identity(["The Rock"])] = { "members": ["The Rock"], "wins": 1, "losses": 0, "recent": ["W"], } pool = app.season_other_pool(season) self.assertNotIn("Kane", pool) self.assertNotIn("The Rock", pool) self.assertIn("Val Venis", pool) self.assertNotIn("Others", app.CHAMPIONSHIPS) def test_ppv_modestly_boosts_title_and_rivalry_booking(self): self.assertGreater(app.title_defense_weight("title", "PPV"), app.title_defense_weight("title", "Raw")) self.assertGreater(app.title_defense_weight("rivalry", "PPV"), app.title_defense_weight("rivalry", "SmackDown")) self.assertEqual(app.title_defense_weight("pool", "PPV"), app.title_defense_weight("pool", "Raw")) def test_title_feud_uses_highest_ranked_eligible_challenger(self): division = "World Wrestling Federation" season = self.season(champions={division: ["The Rock"]}) table = season["rankings"][division] table[app.side_identity(["Kane"])] = { "members": ["Kane"], "wins": 5, "losses": 1, "recent": ["W", "W"], } table[app.side_identity(["Mankind"])] = { "members": ["Mankind"], "wins": 2, "losses": 2, "recent": ["L", "W"], } candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "Raw", set()) title_match = next(candidate["match"] for candidate in candidates if candidate["kind"] == "title" and candidate["match"]["ranking_division"] == division) self.assertEqual(title_match["sides"], [["The Rock"], ["Kane"]]) self.assertTrue(title_match["title_defense"]) def test_vacant_championship_uses_top_two_ranked_contenders(self): division = "European" season = self.season() table = season["rankings"][division] for wrestler, wins in (("Kane", 5), ("Mankind", 3), ("The Rock", 1)): table[app.side_identity([wrestler])] = { "members": [wrestler], "wins": wins, "losses": 0, "recent": ["W"], } candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "Raw", set()) title = next(item["match"] for item in candidates if item["kind"] == "title" and item["match"]["ranking_division"] == division) self.assertEqual(title["sides"], [["Kane"], ["Mankind"]]) self.assertTrue(title["title"].startswith("Vacant ")) self.assertTrue(title["title_defense"]) def test_ppv_rivalry_match_is_marked_as_storyline_payoff(self): season = self.season() sides = [["The Rock"], ["Stone Cold Steve Austin"]] rivalry_id = app.season_rivalry_id(sides) season["rivalries"][rivalry_id] = { "sides": sides, "meetings": 3, "winner_counts": {}, "resolved": False, "ranking_division": "World Wrestling Federation", } candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "PPV", set()) feud = next(candidate["match"] for candidate in candidates if candidate["kind"] == "rivalry") self.assertTrue(feud["rivalry_payoff"]) self.assertEqual(feud["title"], "Rivalry Finale") def test_ppv_title_match_can_resolve_an_existing_title_feud(self): division = "World Wrestling Federation" season = self.season(champions={division: ["The Rock"]}) challenger = ["Kane"] season["rankings"][division][app.side_identity(challenger)] = { "members": challenger, "wins": 3, "losses": 1, "recent": ["W", "W"], } sides = [["The Rock"], challenger] season["rivalries"][app.season_rivalry_id(sides)] = { "sides": sides, "meetings": 1, "winner_counts": {}, "resolved": False, "ranking_division": division, } candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "PPV", set()) title = next(item["match"] for item in candidates if item["kind"] == "title") self.assertTrue(title["title_defense"]) self.assertTrue(title["rivalry_payoff"]) self.assertIn("Feud Finale", title["title"]) def test_booking_caps_title_defenses_at_one_weekly_and_two_on_ppv(self): champions = { "Tag Team": ["Road Dogg", "Billy Gunn"], "World Wrestling Federation": ["The Rock"], "Intercontinental": ["The Undertaker"], "European": ["Kane"], "Hardcore": ["Mankind"], "Light-Heavyweight": ["X-Pac"], "Women's": ["Chyna"], } season = self.season(champions=champions) for division, holder in champions.items(): if division == "Tag Team": challenger = ["Edge", "Christian"] elif division == "Women's": challenger = ["Tori"] else: challenger = ["Val Venis"] season["rankings"][division][app.side_identity(challenger)] = { "members": challenger, "wins": 3, "losses": 1, "recent": ["W", "W"], } with patch.object(app.random, "seed", 6): raw = app.build_season_matches(season, "Raw") ppv = app.build_season_matches(season, "PPV") self.assertLessEqual(sum(bool(match["title_defense"]) for match in raw), 1) self.assertLessEqual(sum(bool(match["title_defense"]) for match in ppv), 2) def test_booking_a_show_persists_current_card_and_cycle_brand(self): with tempfile.TemporaryDirectory() as folder: root = Path(folder) season = self.season(season_id="booking-test") with patch.object(app, "SEASON_DIR", root / "seasons"), \ patch.object(app, "CARD_DIR", root / "cards"), \ contextlib.redirect_stdout(io.StringIO()): card = app.book_next_season_show(season) saved = app.load_season(season["season_id"]) loaded_card = app.load_card(Path(card["path"])) self.assertEqual(card["season_show"], "Raw") self.assertEqual(len(card["matches"]), 3) self.assertEqual(saved["current_card"], card["path"]) self.assertEqual(loaded_card["season_show_number"], 1) def test_season_save_round_trip_and_season_linked_card_metadata(self): with tempfile.TemporaryDirectory() as folder: season_dir, card_dir = Path(folder) / "seasons", Path(folder) / "cards" season = self.season(champions={"European": ["Kane"]}) card = MarkdownTests().make_card(card_dir / "Raw.md") card.update(season_id=season["season_id"], season_show="Raw", season_show_number=1) with patch.object(app, "SEASON_DIR", season_dir), patch.object(app, "CARD_DIR", card_dir): app.save_season(season) app.save_card(card) loaded_season = app.load_season(season["season_id"]) loaded_card = app.load_card(Path(card["path"])) self.assertEqual(loaded_season["champions"]["European"]["holder"], ["Kane"]) self.assertEqual(loaded_card["season_id"], season["season_id"]) self.assertEqual(loaded_card["season_show"], "Raw") self.assertEqual(loaded_card["season_show_number"], 1) def test_syncing_completed_season_card_updates_results_only_once(self): with tempfile.TemporaryDirectory() as folder: root = Path(folder) season = self.season(season_id="sync-test") card = MarkdownTests().make_card(root / "Raw.md") card.update(season_id=season["season_id"], season_show="Raw", season_show_number=1) for match in card["matches"]: match["winner_side"] = 0 match["ranking_division"] = "European" season["current_card"] = str(root / "Raw.md") with patch.object(app, "SEASON_DIR", root / "seasons"): app.save_season(season) app.sync_season_card_results(card) after_first = app.load_season(season["season_id"]) app.sync_season_card_results(card) after_second = app.load_season(season["season_id"]) self.assertEqual(after_first["next_show_number"], 2) self.assertEqual(after_first["current_card"], "") self.assertEqual(len(after_first["processed_results"]), 3) self.assertEqual(after_second["rankings"], after_first["rankings"]) self.assertEqual(len(after_second["processed_results"]), 3) class SeasonControlTests(unittest.TestCase): def season(self, season_id="control-test"): return app.new_season_state("Control Test", "sd2", season_id=season_id) def test_new_and_legacy_seasons_default_to_booker_control(self): season = self.season() self.assertEqual(season["control_mode"], "booker") self.assertIsNone(season["player_wrestler"]) with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder)): season.pop("control_mode") season.pop("player_wrestler") app.save_season(season) loaded = app.load_season(season["season_id"]) self.assertEqual(loaded["control_mode"], "booker") self.assertIsNone(loaded["player_wrestler"]) def test_startup_choice_and_toggle_persist_wrestler_then_booker(self): season = self.season() expected_wrestler = sorted(app.load_roster("sd2"), key=str.casefold)[0] with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder)), \ patch.object(app, "ask_choice", side_effect=[1, 0]): app.choose_season_control_mode(season) self.assertEqual(season["control_mode"], "wrestler") self.assertEqual(season["player_wrestler"], expected_wrestler) self.assertEqual(app.load_season(season["season_id"])["player_wrestler"], expected_wrestler) app.toggle_season_control_mode(season) self.assertEqual(app.load_season(season["season_id"])["control_mode"], "booker") with patch.object(app, "ask_choice", return_value=0): app.toggle_season_control_mode(season) saved = app.load_season(season["season_id"]) self.assertEqual(saved["control_mode"], "wrestler") self.assertEqual(saved["player_wrestler"], expected_wrestler) def test_season_menu_displays_current_control_mode_and_toggles_back(self): season = self.season() season["control_mode"] = "wrestler" season["player_wrestler"] = "The Rock" with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder)), \ patch.object(app, "ask_choice", side_effect=[2, 3]) as choose, \ contextlib.redirect_stdout(io.StringIO()): app.save_season(season) app.run_season(season) saved = app.load_season(season["season_id"]) self.assertIn("Toggle Control Mode — Current: Wrestler (The Rock)", choose.call_args_list[0].kwargs["labels"]) self.assertEqual(season["control_mode"], "booker") self.assertEqual(saved["control_mode"], "booker") def test_wrestler_control_simulates_non_player_matches_and_preserves_player_match(self): with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder) / "seasons"): season = self.season("wrestler-control-results") season["control_mode"] = "wrestler" season["player_wrestler"] = "The Rock" card = MarkdownTests().make_card(Path(folder) / "card.md") card["season_id"] = season["season_id"] card["season_show_number"] = 1 card["title_definitions"] = season["title_definitions"] card["matches"][0].update({ "sides": [["The Rock"], ["Kane"]], "winner_side": None, }) card["matches"][1].update({ "sides": [["Edge"], ["Christian"]], "winner_side": None, }) card["matches"][2].update({ "sides": [["Tazz"], ["Undertaker"]], "winner_side": None, }) season["current_card"] = card["path"] app.save_season(season) app.save_card(card) with contextlib.redirect_stdout(io.StringIO()): self.assertFalse(app.record_match_result(card, 1, auto_simulate=False)) app.simulate_non_player_matches(card, season) self.assertIsNone(card["matches"][0]["winner_side"]) self.assertIsNotNone(card["matches"][1]["winner_side"]) self.assertIsNotNone(card["matches"][2]["winner_side"]) with contextlib.redirect_stdout(io.StringIO()): self.assertFalse(app.record_match_result(card, 0, auto_simulate=True)) captured = {} def exit_card(_prompt, choices, labels=None): captured["choices"] = choices captured["labels"] = labels return 4 with patch.object(app, "ask_choice", side_effect=exit_card), \ contextlib.redirect_stdout(io.StringIO()): self.assertTrue(app.resume_card(card)) self.assertNotIn("edit", captured["choices"]) self.assertNotIn("simulate all", captured["choices"]) self.assertIn("Record your match result", captured["labels"]) with patch.object(app, "ask_choice", return_value=0), \ patch.object(app, "choose_match_outcome", return_value="Pinfall"), \ patch.object(app, "prompt_match_minutes", return_value=10), \ patch.object(app, "prompt_action_count", return_value=1), \ contextlib.redirect_stdout(io.StringIO()): app.record_result(card, match_index=0) self.assertEqual(card["matches"][0]["winner_side"], 0) def test_absent_wrestler_causes_all_pending_matches_to_be_simulated(self): with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder) / "seasons"): season = self.season("wrestler-absent") season["control_mode"] = "wrestler" season["player_wrestler"] = "The Rock" card = MarkdownTests().make_card(Path(folder) / "card.md") card["season_id"] = season["season_id"] card["season_show_number"] = 1 card["title_definitions"] = season["title_definitions"] for match, sides in zip(card["matches"], [ [["Kane"], ["Undertaker"]], [["Edge"], ["Christian"]], [["Tazz"], ["Big Show"]], ]): match.update({"sides": sides, "winner_side": None}) season["current_card"] = card["path"] app.save_season(season) app.save_card(card) with contextlib.redirect_stdout(io.StringIO()): app.simulate_non_player_matches(card, season) self.assertTrue(all(match["winner_side"] is not None for match in card["matches"])) updated = app.load_season(season["season_id"]) self.assertEqual(updated["current_card"], "") self.assertEqual(updated["next_show_number"], 2) class LightHeavyweightTests(unittest.TestCase): def test_builtin_season_selects_and_snapshots_eligible_wrestlers(self): def select_eligible(profile): profile["light_heavyweight_roster"] = ["X-Pac", "Jeff Hardy"] def initial_champion(division, _game_id, _used, _profile): return ["X-Pac"] if division == "Light-Heavyweight" else None with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder)), \ patch.object(app, "choose_game", return_value="sd1"), \ patch.object(app, "ask", return_value="Lightweight Season"), \ patch.object(app, "edit_light_heavyweight_roster", side_effect=select_eligible), \ patch.object(app, "choose_initial_champion", side_effect=initial_champion), \ contextlib.redirect_stdout(io.StringIO()): season = app.create_season() loaded = app.load_season(season["season_id"]) self.assertEqual(season["game_config"]["light_heavyweight_roster"], ["X-Pac", "Jeff Hardy"]) self.assertEqual( app.eligible_roster_for_division( "sd1", "Light-Heavyweight", profile=loaded["game_config"] ), ["X-Pac", "Jeff Hardy"], ) self.assertEqual(loaded["champions"]["Light-Heavyweight"]["holder"], ["X-Pac"]) defense = { "ranking_division": "Light-Heavyweight", "title_defense": True, "sides": [["X-Pac"], ["Jeff Hardy"]], "winner_side": 1, } self.assertTrue(app.validate_title_defense(loaded, defense)) app.record_season_match(loaded, {"season_show": "Raw"}, defense, 0) self.assertEqual(loaded["champions"]["Light-Heavyweight"]["holder"], ["Jeff Hardy"]) def test_initial_champion_random_choice_uses_only_eligible_wrestlers(self): profile = app.game_profile("sd1") profile["light_heavyweight_roster"] = ["X-Pac"] choices_seen = [] def first_choice(options): choices_seen.append(list(options)) return options[0] with patch.object(app, "ask_choice", return_value=1), \ patch.object(app.random, "choice", side_effect=first_choice), \ contextlib.redirect_stdout(io.StringIO()): champion = app.choose_initial_champion( "Light-Heavyweight", "sd1", set(), profile ) self.assertEqual(champion, ["X-Pac"]) self.assertEqual(choices_seen, [["X-Pac"]]) def test_ineligible_wrestlers_cannot_record_or_sync_division_results(self): with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder)): season = app.new_season_state( "Lightweight Season", "sd1", champions={"Light-Heavyweight": ["X-Pac"]}, season_id="lightweight-results", ) season["game_config"]["light_heavyweight_roster"] = ["X-Pac"] card_path = Path(folder) / "card.md" season["current_card"] = str(card_path) app.save_season(season) match = { "type": "Singles", "sides": [["X-Pac"], ["The Rock"]], "winner_side": 1, "ranking_division": "Light-Heavyweight", "title_defense": True, } card = { "season_id": season["season_id"], "season_show_number": 1, "season_show": "Raw", "path": str(card_path), "matches": [match], } with contextlib.redirect_stdout(io.StringIO()): self.assertFalse(app.validate_title_defense(season, match)) self.assertFalse(app.record_match_result(card, 0, auto_simulate=True)) app.sync_season_card_results(card) updated = app.load_season(season["season_id"]) self.assertEqual(updated["champions"]["Light-Heavyweight"]["holder"], ["X-Pac"]) self.assertEqual(updated["rankings"]["Light-Heavyweight"], {}) self.assertEqual(updated["processed_results"], []) def test_legacy_season_without_eligibility_snapshot_remains_unrestricted(self): season = app.new_season_state("Legacy Season", "sd1", season_id="legacy-lightweight") season["game_config"].pop("light_heavyweight_roster", None) self.assertEqual( app.eligible_roster_for_division( "sd1", "Light-Heavyweight", profile=season["game_config"] ), season["game_config"]["roster"], ) def test_legacy_builtin_light_heavyweight_kind_migrates_without_restricting_roster(self): season = app.new_season_state( "Legacy Lightweight", "sd1", season_id="legacy-lightweight-kind" ) for definitions in (season["title_definitions"], season["game_config"]["championships"]): next(item for item in definitions if item["name"] == "Light-Heavyweight")["kind"] = "singles" season["game_config"].pop("light_heavyweight_roster", None) with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder)): app.save_season(season) loaded = app.load_season(season["season_id"]) self.assertEqual( app.season_championship_kind(loaded, "Light-Heavyweight"), "light_heavyweight", ) self.assertEqual( app.eligible_roster_for_division( "sd1", "Light-Heavyweight", profile=loaded["game_config"] ), loaded["game_config"]["roster"], ) class BYOGTests(unittest.TestCase): def setUp(self): self._configs = app.CUSTOM_GAME_CONFIGS self._games = dict(app.GAMES) self._match_types = dict(app.MATCH_TYPES) # Point the BYOG directory at a session-local temp dir so custom # registrations survive refreshes and never touch the real edit/ folder. self.game_dir = tempfile.TemporaryDirectory() self.addCleanup(self.game_dir.cleanup) patcher = patch.object(app, "CUSTOM_GAME_DIR", Path(self.game_dir.name) / "games") patcher.start() self.addCleanup(patcher.stop) def tearDown(self): app.CUSTOM_GAME_CONFIGS = self._configs app.GAMES.clear() app.GAMES.update(self._games) app.MATCH_TYPES.clear() app.MATCH_TYPES.update(self._match_types) def config(self, **overrides) -> dict: data = { "game_id": "custom-fantasy-fed-rumble-royale", "game_title": "Rumble Royale", "federation_name": "Fantasy Fed", "platform": "ps1", "roster": ["Ava Crusher", "Ben Brawler", "Cara Storm", "Dash Vance"], "match_types": [ {"name": "Scaffold", "sides": [1, 1], "base_rating": 8.5}, {"name": "Tag Tornado", "sides": [2, 2], "outcomes": ["pinfall", "submission"]}, ], "championships": [ {"name": "Rumble Belt", "kind": "singles"}, {"name": "Fantasy Tag", "kind": "tag"}, {"name": "Fantasy Women", "kind": "women"}, ], "women_roster": ["Cara Storm"], "light_heavyweight_roster": [], } data.update(overrides) return data def register(self, data: dict) -> dict: saved = app.save_custom_game(data, app.CUSTOM_GAME_DIR) app.refresh_custom_games() return saved def filler_match(self, match_type: str, base_rating: float | None = None) -> dict: sized = {} for item in app.match_type_definitions(self.config()["game_id"]): if item["name"] == match_type: sized = item break return {"title": "", "type": match_type, "sides": [], "winner_side": None, "finish": "", "promo": None, "image": "", "ranking_division": None, "title_defense": False, "rivalry_payoff": False, "match_time_minutes": None, "near_falls": 0, "finishers": 0, "rating": None, "base_rating": base_rating if base_rating is not None else (sized.get("base_rating") if sized else app.base_match_rating(match_type)), "valid_outcomes": list(sized["outcomes"]) if sized else list(app.STANDARD_MATCH_OUTCOMES)} def test_validate_custom_game_accepts_and_normalizes_profiles(self): saved = app.validate_custom_game(self.config()) self.assertEqual(saved["game_id"], "custom-fantasy-fed-rumble-royale") self.assertEqual(saved["schema_version"], 1) self.assertEqual(saved["match_types"][0]["base_rating"], 8.5) self.assertEqual(saved["match_types"][1]["base_rating"], app.MATCH_BASE_RATINGS["Tag Team"]) self.assertEqual(saved["match_types"][0]["outcomes"], list(app.STANDARD_MATCH_OUTCOMES)) self.assertEqual(saved["match_types"][1]["outcomes"], ["pinfall", "submission"]) self.assertEqual(saved["championships"][2]["kind"], "women") self.assertIsNone(saved["screenshot_directory"]) saved_with_override = app.validate_custom_game( self.config(screenshot_directory=" ~/Game Shots ") ) self.assertEqual(saved_with_override["screenshot_directory"], "~/Game Shots") def test_validate_custom_game_rejects_invalid_definitions(self): rejects = [ ({"game_id": "sd1"}, "custom definitions cannot overwrite built-in games"), ({"game_id": "Bad ID"}, "game_id must use lowercase"), ({"roster": ["Ava Crusher", "ava crusher"]}, "unique"), ({"roster": ["Solo"]}, "at least two"), ({"women_roster": ["Not Listed"]}, "subset of the roster"), ({"match_types": [{"name": "Tag Tornado", "sides": [3, 3]}]}, "invalid side sizes"), ({"match_types": []}, "at least one match type"), ({"championships": [{"name": "Belt", "kind": "hardcore"}]}, "kind"), ({"light_heavyweight_roster": ["Not Listed"]}, "subset of the roster"), ({"platform": "dreamcast"}, "platform must be ps1"), ({"screenshot_directory": 3}, "screenshot_directory must be a path string"), ] for overrides, message in rejects: with self.subTest(overrides=overrides): data = self.config(**overrides) with self.assertRaises(ValueError) as caught: app.validate_custom_game(data) self.assertIn(message, str(caught.exception).lower()) def test_custom_games_use_platform_defaults_or_expanded_overrides(self): with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "CUSTOM_GAME_DIR", Path(folder) / "games"): ps1 = self.config(game_id="custom-shot-ps1", platform="ps1") ps2 = self.config(game_id="custom-shot-ps2", platform="ps2") other = self.config(game_id="custom-shot-other", platform="other") overridden = self.config( game_id="custom-shot-override", platform="other", screenshot_directory="~/GameScreens", ) ps2_override = self.config( game_id="custom-shot-ps2-override", platform="ps2", screenshot_directory="~/PCSX2Alt", ) relative = self.config( game_id="custom-shot-relative", platform="other", screenshot_directory="custom-screens", ) for config in (ps1, ps2, other, overridden, ps2_override, relative): app.save_custom_game(config, app.CUSTOM_GAME_DIR) app.refresh_custom_games() self.assertEqual(app.screenshot_directory_for_game(ps1["game_id"]), app.DUCKSTATION_SCREENSHOT_DIR) self.assertEqual(app.screenshot_directory_for_game(ps2["game_id"]), app.PCSX2_SCREENSHOT_DIR) self.assertIsNone(app.screenshot_directory_for_game(other["game_id"])) self.assertFalse(app.game_has_screenshots(other["game_id"])) self.assertEqual(app.screenshot_directory_for_game(overridden["game_id"]), Path.home() / "GameScreens") self.assertEqual(app.screenshot_directory_for_game(ps2_override["game_id"]), Path.home() / "PCSX2Alt") self.assertEqual(app.screenshot_directory_for_game(relative["game_id"]), app.ROOT / "custom-screens") self.assertTrue(app.game_has_screenshots(overridden["game_id"])) def test_custom_light_heavyweight_eligibility_is_validated_and_applied(self): config = self.config( championships=[ {"name": "Fantasy Lightweight", "kind": "light_heavyweight"}, ], light_heavyweight_roster=["Ava Crusher", "Cara Storm"], ) self.register(config) profile = app.game_profile(config["game_id"]) self.assertEqual( app.eligible_roster_for_division( config["game_id"], "Fantasy Lightweight", profile=profile ), ["Ava Crusher", "Cara Storm"], ) self.assertEqual( app.championship_kind(config["game_id"], "Fantasy Lightweight", profile), "light_heavyweight", ) with self.assertRaisesRegex(ValueError, "must contain eligible wrestlers"): app.validate_custom_game(self.config(championships=[ {"name": "Fantasy Lightweight", "kind": "light_heavyweight"}, ])) def test_light_heavyweight_eligibility_editor_selects_roster_members(self): config = {"roster": ["Ava Crusher", "Ben Brawler"], "light_heavyweight_roster": []} with patch.object(app, "ask_choice", side_effect=[0, 0, 2]), \ contextlib.redirect_stdout(io.StringIO()): app.edit_light_heavyweight_roster(config) self.assertEqual(config["light_heavyweight_roster"], ["Ava Crusher"]) def test_custom_game_screenshot_override_can_be_set_in_editor(self): config = app.validate_custom_game(self.config()) with patch.object(app, "ask_choice", side_effect=[0, 0, 6]), \ patch.object(app, "ask", side_effect=["Rumble Royale", "Fantasy Fed", "~/Shots"]), \ patch.object(app, "save_custom_game", side_effect=lambda value: value): app.edit_custom_profile(config) self.assertEqual(config["screenshot_directory"], "~/Shots") def test_saved_custom_game_registers_in_catalog(self): saved = self.register(self.config()) self.assertTrue((app.CUSTOM_GAME_DIR / f"{saved['game_id']}.json").is_file()) game_id = saved["game_id"] self.assertIn(game_id, app.GAMES) self.assertIn(game_id, app.CUSTOM_GAME_CONFIGS) self.assertNotIn(game_id, app.BUILTIN_GAME_IDS) self.assertEqual(app.load_roster(game_id), saved["roster"]) self.assertEqual(app.game_title(game_id), "Rumble Royale") self.assertEqual(app.game_display_name(game_id), "Fantasy Fed — Rumble Royale") self.assertEqual(app.championship_names_for_game(game_id), ["Rumble Belt", "Fantasy Tag", "Fantasy Women"]) self.assertTrue(app.game_has_screenshots(game_id)) self.assertEqual(app.eligible_roster_for_division(game_id, "Fantasy Women"), ["Cara Storm"]) self.assertEqual([item["name"] for item in app.match_type_definitions(game_id)], ["Scaffold", "Tag Tornado"]) def test_custom_game_survives_relaunch_via_refresh(self): self.register(self.config()) self.assertEqual(app.refresh_custom_games(), ["custom-fantasy-fed-rumble-royale"]) self.assertEqual(app.refresh_custom_games(), ["custom-fantasy-fed-rumble-royale"]) def test_custom_game_card_round_trip_keeps_game_metadata(self): game_id = self.config()["game_id"] self.register(self.config()) with tempfile.TemporaryDirectory() as folder: path = Path(folder) / "card.md" card = { "game_id": game_id, "show": "Rumble Night", "date": "2026-11-02", "matches": [self.filler_match("Scaffold", base_rating=8.5), self.filler_match("Tag Tornado"), self.filler_match("Tag Tornado")], "path": str(path), } app.save_card(card) loaded = app.load_card(path) self.assertEqual(loaded["game_title"], "Rumble Royale") self.assertEqual(loaded["federation_name"], "Fantasy Fed") self.assertEqual([item["name"] for item in loaded["title_definitions"]], ["Rumble Belt", "Fantasy Tag", "Fantasy Women"]) self.assertEqual(loaded["matches"][0]["type"], "Scaffold") self.assertEqual(loaded["matches"][0]["base_rating"], 8.5) self.assertEqual(loaded["matches"][1]["valid_outcomes"], ["pinfall", "submission"]) def test_custom_match_base_rating_can_drive_the_match_rating(self): match = {"type": "Scaffold", "base_rating": 8.5, "match_time_minutes": 12.9, "near_falls": 2, "finishers": 3, "title_defense": False} plain = dict(match, base_rating=None) self.assertEqual(app.calculate_match_rating(match) - app.calculate_match_rating(plain), 8.5 - app.base_match_rating("Scaffold")) def test_custom_game_season_uses_custom_title_divisions(self): config = self.config() game_id = config["game_id"] self.register(config) season = app.new_season_state("Fantasy Season", game_id, champions={"Rumble Belt": ["Ava Crusher"], "Fantasy Tag": ["Ben Brawler", "Dash Vance"]}, season_id="fantasy-season") self.assertEqual([item["name"] for item in season["title_definitions"]], ["Rumble Belt", "Fantasy Tag", "Fantasy Women"]) self.assertEqual(set(season["rankings"]), {"Rumble Belt", "Fantasy Tag", "Fantasy Women"}) self.assertEqual(season["champions"]["Rumble Belt"]["holder"], ["Ava Crusher"]) with contextlib.redirect_stdout(io.StringIO()): roster = app.load_roster(game_id) matches = app.build_season_matches(season, "Raw") self.assertEqual(len(matches), 3) for match in matches: self.assertIn(match["ranking_division"], (None, "Rumble Belt", "Fantasy Tag", "Fantasy Women")) # A custom division result updates the custom rankings. match = {"type": "Scaffold", "sides": [["Ava Crusher"], ["Ben Brawler"]], "winner_side": 0, "ranking_division": "Rumble Belt", "title_defense": False} app.record_season_match(season, {"season_show": "Raw"}, match, 0) table = season["rankings"]["Rumble Belt"] self.assertEqual(table[app.side_identity(["Ava Crusher"])]["wins"], 1) self.assertEqual(table[app.side_identity(["Ben Brawler"])]["losses"], 1) # A successful defense keeps the custom champion on top. defense = {"type": "Scaffold", "sides": [["Ava Crusher"], ["Cara Storm"]], "winner_side": 0, "ranking_division": "Rumble Belt", "title_defense": True} app.record_season_match(season, {"season_show": "Raw"}, defense, 0) self.assertEqual(season["champions"]["Rumble Belt"]["holder"], ["Ava Crusher"]) self.assertEqual(season["champions"]["Rumble Belt"]["successful_defenses"], 1) def test_custom_season_save_round_trip_keeps_title_definitions(self): self.register(self.config()) season = app.new_season_state("Fantasy Season", self.config()["game_id"], champions={"Rumble Belt": ["Ava Crusher"]}, season_id="fantasy-save") with tempfile.TemporaryDirectory() as folder: with patch.object(app, "SEASON_DIR", Path(folder)): app.save_season(season) loaded = app.load_season(season["season_id"]) self.assertEqual(loaded["game_title"], "Rumble Royale") self.assertEqual(loaded["federation_name"], "Fantasy Fed") self.assertEqual(loaded["champions"]["Rumble Belt"]["holder"], ["Ava Crusher"]) self.assertEqual([item["name"] for item in loaded["title_definitions"]], ["Rumble Belt", "Fantasy Tag", "Fantasy Women"]) self.assertEqual(loaded["shows"], app.DEFAULT_CUSTOM_SHOWS) def test_choose_game_lists_builtin_games_then_custom_games(self): self.register(self.config()) with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", return_value=len(app.GAMES) - 1): self.assertEqual(app.choose_game(), self.config()["game_id"]) CUSTOM_SHOWS = {"Raw": "Fantasy Rampage", "SmackDown": "Fantasy Clash", "PPV": "Fantasy Super Card"} def test_shows_validation_accepts_normalizes_and_rejects_bad_values(self): saved = app.validate_custom_game(self.config(shows=self.CUSTOM_SHOWS)) self.assertEqual(saved["shows"], self.CUSTOM_SHOWS) # Absent shows fall back to neutral placeholders, never WWE names. self.assertEqual(app.validate_custom_game(self.config())["shows"], app.DEFAULT_CUSTOM_SHOWS) rejects = [ ({"shows": ["Fantasy Rampage"]}, "shows must be an object"), ({"shows": {"Raw": " "}}, "non-empty string"), ({"shows": self.CUSTOM_SHOWS | {"Raw": "fantasy clash"}}, "unique"), ({"shows": {"Raw": 3}}, "non-empty string"), ] for overrides, message in rejects: with self.subTest(overrides=overrides): data = self.config(**overrides) with self.assertRaises(ValueError) as caught: app.validate_custom_game(data) self.assertIn(message, str(caught.exception).lower()) def test_game_show_names_resolve_wwe_names_only_for_builtins(self): self.assertEqual(app.game_show_names("sd1")["SmackDown"], "Thursday Night SmackDown!") self.register(self.config()) self.assertEqual(app.game_show_names(self.config()["game_id"]), app.DEFAULT_CUSTOM_SHOWS) self.register(self.config(shows=self.CUSTOM_SHOWS)) game_id = self.config()["game_id"] self.assertEqual(app.game_show_names(game_id), self.CUSTOM_SHOWS) def test_built_in_clone_prefills_wwe_show_names(self): profile = app.builtin_game_profile("sd2") self.assertEqual(profile["shows"]["Raw"], "Monday Night Raw") copied = app.builtin_profile_for_copy("sd2", "custom-clone-test") self.assertEqual(copied["shows"], {"Raw": "Monday Night Raw", "SmackDown": "Thursday Night SmackDown!", "PPV": "WWF PPV"}) def test_booked_season_cards_use_custom_show_names(self): game_id = self.register(self.config(shows=self.CUSTOM_SHOWS))["game_id"] season = app.new_season_state("Show Name Season", game_id, champions={}, season_id="showname-season") self.assertEqual(season["shows"], self.CUSTOM_SHOWS) with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "SEASON_DIR", Path(folder) / "seasons"), \ patch.object(app, "CARD_DIR", Path(folder) / "cards"), \ contextlib.redirect_stdout(io.StringIO()): names = [] for show_number in (1, 2, 3): season["next_show_number"] = show_number card = app.book_next_season_show(season) names.append(card["show"]) season["current_card"] = "" card["matches"][0]["winner_side"] = 0 season_path = app.season_path(season["season_id"]) # The season save keeps its custom show-name snapshot on disk. saved = json.loads(season_path.read_text("utf-8")) self.assertEqual(saved["shows"], self.CUSTOM_SHOWS) self.assertEqual(names, ["Fantasy Rampage", "Fantasy Clash", "Fantasy Super Card"]) def test_is_ppv_card_matches_custom_ppv_show_name_exactly(self): self.register(self.config(shows=self.CUSTOM_SHOWS)) game_id = self.config()["game_id"] event_card = {"game_id": game_id, "show": "Fantasy Super Card"} weekly_card = {"game_id": game_id, "show": "Fantasy Rampage"} self.assertTrue(app.is_ppv_card(event_card)) self.assertFalse(app.is_ppv_card(weekly_card)) # Case-insensitive exact match still applies. self.assertTrue(app.is_ppv_card({"game_id": game_id, "show": "fantasy super card"})) # Unknown card shows stay non-PPV even with a custom game. self.assertFalse(app.is_ppv_card({"game_id": game_id, "show": "House Party"})) def test_one_off_cards_use_game_show_names(self): game_id = self.register(self.config(shows=self.CUSTOM_SHOWS))["game_id"] with tempfile.TemporaryDirectory() as folder, \ patch.object(app, "CARD_DIR", Path(folder) / "cards"), \ contextlib.redirect_stdout(io.StringIO()): with patch.object(app, "choose_game", return_value=game_id): card = app.create_random_card() self.assertIn(card["show"], set(self.CUSTOM_SHOWS.values())) # The create-a-card show prompt defaults to the second weekly slot. captured = {} class Stop(Exception): pass def fake_ask(prompt, default=None): captured["default"] = default raise Stop() with patch.object(app, "choose_game", return_value=game_id), \ patch.object(app, "ask", fake_ask), \ self.assertRaises(Stop): app.create_card() self.assertEqual(captured["default"], "Fantasy Clash") def test_edit_custom_shows_renames_and_rejects_duplicates(self): config = {"shows": dict(self.CUSTOM_SHOWS)} with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", side_effect=[0, 3]), \ patch.object(app, "ask", return_value="Fantasy Festival"): app.edit_custom_shows(config) self.assertEqual(config["shows"]["Raw"], "Fantasy Festival") config = {"shows": dict(self.CUSTOM_SHOWS)} with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", side_effect=[1, 3]), \ patch.object(app, "ask", side_effect=["Fantasy Rampage", "Fantasy Zoom"]): app.edit_custom_shows(config) self.assertEqual(config["shows"]["SmackDown"], "Fantasy Zoom") def test_view_roster_lists_names_read_only(self): config = {"roster": ["Zulu Viper", "Ava Crusher", "Ben Brawler"], "women_roster": ["Ava Crusher"]} captured = io.StringIO() with contextlib.redirect_stdout(captured), \ patch.object(app, "ask_choice", side_effect=[2, 3]): app.edit_custom_roster(config) text = captured.getvalue() self.assertIn("Roster (3 wrestlers):", text) self.assertIn(" 1. Ava Crusher", text) self.assertIn(" 2. Ben Brawler", text) self.assertIn(" 3. Zulu Viper", text) self.assertEqual(config["roster"], ["Zulu Viper", "Ava Crusher", "Ben Brawler"]) self.assertEqual(config["women_roster"], ["Ava Crusher"]) def test_view_roster_on_empty_roster(self): config = {"roster": [], "women_roster": []} captured = io.StringIO() with contextlib.redirect_stdout(captured), \ patch.object(app, "ask_choice", side_effect=[2, 3]): app.edit_custom_roster(config) self.assertIn("The roster is empty.", captured.getvalue()) def test_view_roster_dumps_large_roster_completely(self): config = {"roster": [f"Wrestler {index:02d}" for index in range(65)], "women_roster": []} captured = io.StringIO() with contextlib.redirect_stdout(captured), \ patch.object(app, "ask_choice", side_effect=[2, 3]): app.edit_custom_roster(config) text = captured.getvalue() self.assertIn("Roster (65 wrestlers):", text) self.assertIn(" 1. Wrestler 00", text) self.assertIn(" 65. Wrestler 64", text) def test_roster_menu_ordering_adds_view_between_remove_and_done(self): seen = {} config = {"roster": ["Ava Crusher"], "women_roster": []} def capturing_choice(prompt, choices, default=None, labels=None): if prompt == "Edit roster": seen["labels"] = labels return len(choices) - 1 with contextlib.redirect_stdout(io.StringIO()), \ patch.object(app, "ask_choice", side_effect=capturing_choice): app.edit_custom_roster(config) self.assertEqual(seen["labels"], ["Add a wrestler", "Remove a wrestler", "View roster (read-only)", "Done (1 listed)"]) class FlashRenderingTests(unittest.TestCase): def test_flash_updates_only_changed_rows_not_static_header(self): card = MarkdownTests().make_card(Path("card.md")) with patch.object(app, "_render_image", return_value=([], 0)): normal = app.render_card_lines(card) flashing = app.render_card_lines(card, flash=True) header_length = len(app.header_lines(card, 100)) self.assertEqual(normal[:header_length], flashing[:header_length]) output = io.StringIO() app.write_changed_lines(normal, flashing, stream=output) updates = output.getvalue() self.assertNotIn("WWF.com", updates) self.assertNotIn("SMACKDOWN!", updates) self.assertNotIn("\033[2J", updates) self.assertIn("WIN!", updates) self.assertIn("LOSE", updates) def test_full_card_layout_fits_typical_terminal_height(self): card = MarkdownTests().make_card(Path("card.md")) card["matches"][2]["sides"] = [["Stone Cold Steve Austin"], ["Kane"]] for match in card["matches"]: match["promo"] = "First speaker: A short line.\nSecond: A reply.\nFirst: A close." with (patch.object(app, "_render_image", side_effect=[(["logo"] * 6, 23), (["logo"] * 6, 38)]), patch.object(app.shutil, "get_terminal_size", return_value=os.terminal_size((100, 24)))): lines = app.render_card_lines(card) self.assertLessEqual(len(lines), 24) class HeaderLogoTests(unittest.TestCase): def test_logo_selection_is_case_insensitive_and_show_dependent(self): self.assertEqual(app.show_logo_path("Friday Night SmAcKdOwN"), app.ROOT / "smackdown-logo-source.png") self.assertEqual(app.show_logo_path("Monday Night RAW"), app.ROOT / "raw-is-war-logo.png") self.assertIsNone(app.show_logo_path("WrestleMania")) def test_smackdown_takes_precedence_when_both_keywords_appear(self): self.assertEqual(app.show_logo_path("Raw vs SmackDown"), app.ROOT / "smackdown-logo-source.png") def test_selected_logo_has_text_fallback_when_images_cannot_render(self): card = MarkdownTests().make_card(Path("card.md")) card["show"] = "Monday Night Raw" with patch.object(app, "_render_image", return_value=([], 0)): header = "\n".join(app.header_lines(card, 100)) self.assertIn("RAW IS WAR", header) def custom_card(self) -> dict: card = MarkdownTests().make_card(Path("card.md")) card["federation_name"] = "Fantasy Fed" card["game_title"] = "Rumble Royale" return card def test_is_wwf_federation_matches_canonical_name_and_alias(self): for name in ("World Wrestling Federation", "world wrestling federation", "WWF", " wwf "): self.assertTrue(app.is_wwf_federation(name), name) for name in ("Fantasy Fed", "WWE", "WCW", ""): self.assertFalse(app.is_wwf_federation(name), name) def test_corner_logo_only_renders_for_wwf_federations(self): def image_mock(path, _columns, _rows): if path.name == "wwfcomlogo.gif": return ["IMG1"], 3 return ["C1"], 3 wwf_card = MarkdownTests().make_card(Path("card.md")) wwf_card["show"] = "Monday Night Raw" custom_card = self.custom_card() custom_card["show"] = "Monday Night Raw" with patch.object(app, "_render_image", side_effect=image_mock): wwf_header = app.header_lines(wwf_card, 100) custom_header = app.header_lines(custom_card, 100) self.assertIn("IMG1", wwf_header[0]) self.assertFalse(any("IMG1" in line for line in custom_header)) # With no corner logo the centered show logo is truly centered (48 = (100-3)//2). # With no corner logo the centered show logo is truly centered (48 = (100-3)//2) # and the date still sits at the top-right of the same row. self.assertTrue(custom_header[0].startswith(" " * 48 + "C1"), custom_header[0]) self.assertIn("2026-10-25", custom_header[0]) self.assertNotIn("WWF.com", "\n".join(app.header_lines(custom_card, 100))) def test_wwf_alias_federations_render_the_corner_logo(self): image_mock = lambda path, _columns, _rows: (["IMG1"], 3) for federation in ("WWF", "world wrestling federation"): card = self.custom_card() card["federation_name"] = federation with patch.object(app, "_render_image", side_effect=image_mock): header = app.header_lines(card, 100) self.assertIn("IMG1", header[0], federation) def test_corner_logo_text_fallback_only_for_wwf_federations(self): wwf_card = MarkdownTests().make_card(Path("card.md")) custom_card = self.custom_card() with patch.object(app, "_render_image", return_value=([], 0)): wwf_header = "\n".join(app.header_lines(wwf_card, 100)) custom_header = "\n".join(app.header_lines(custom_card, 100)) self.assertIn("WWF.com", wwf_header) self.assertNotIn("WWF.com", custom_header) def test_narrow_custom_federation_header_drops_wwf_branding(self): wwf_card = MarkdownTests().make_card(Path("card.md")) custom_card = self.custom_card() wwf_header = "\n".join(app.header_lines(wwf_card, 60)) custom_header = "\n".join(app.header_lines(custom_card, 60)) self.assertIn("WWF.com", wwf_header) self.assertNotIn("WWF.com", custom_header) self.assertIn("Rumble Royale", custom_header) for header in (wwf_header, custom_header): self.assertIn("2026-10-25", header) class PromoFoldingTests(unittest.TestCase): def card_with_promos(self) -> dict: card = MarkdownTests().make_card(Path("card.md")) card["matches"][2]["sides"] = [["Stone Cold Steve Austin"], ["Kane"]] for match in card["matches"]: match["promo"] = ( "The Rock: A deliberately long promo line that should wrap when expanded " "instead of being clipped off at the edge of the terminal.\n" "Jeff Hardy: A second complete response with enough detail to test folding.\n" "The Rock: The final promo line remains available in the expanded view." ) return card def test_collapsed_promo_can_expand_to_all_wrapped_lines(self): card = self.card_with_promos() collapsed = app.match_lines(card["matches"][0], 1, 80, promo_selected=True) expanded = app.match_lines( card["matches"][0], 1, 80, promo_selected=True, promo_expanded=True ) expanded_text = "\n".join(expanded) self.assertTrue(any("▶ Promo:" in line for line in collapsed)) self.assertTrue(any("\033[7m" in line for line in collapsed)) self.assertTrue(any("▼ Promo (Enter to collapse)" in line for line in expanded)) self.assertIn("second complete response", expanded_text.lower()) self.assertIn("final promo line remains available", expanded_text) self.assertGreater(len(expanded), len(collapsed)) def test_expanded_view_scrolls_body_but_keeps_header_pinned(self): card = self.card_with_promos() image_lines = ["logo"] * 6 image_mock = lambda path, _columns, _rows: ( image_lines, 23 if path.name == "wwfcomlogo.gif" else 38 ) with patch.object(app, "_render_image", side_effect=image_mock): collapsed, _, _, _ = app.interactive_card_frame(card, 100, 24) expanded, offset, capacity, spans = app.interactive_card_frame( card, 100, 24, selected_promo=0, expanded_promos={0, 1, 2}, keep_selection_visible=True, ) header = app.header_lines(card, 100) self.assertEqual(len(expanded), 24) self.assertGreater(offset, 0) self.assertEqual(collapsed[:len(header)], expanded[:len(header)]) control_row, _ = spans[0] view_row = len(header) + control_row - offset self.assertIn("\033[7m", expanded[view_row]) self.assertGreaterEqual(capacity, 1) def test_terminal_key_reader_maps_enter_arrows_and_page_keys(self): for sequence, expected in ((b"\r", "enter"), (b"\x1b[A", "up"), (b"\x1b[B", "down"), (b"\x1b[5~", "pageup"), (b"\x1b[6~", "pagedown"), (b"q", "quit")): read_fd, write_fd = os.pipe() try: os.write(write_fd, sequence) self.assertEqual(app.read_view_key(read_fd), expected) finally: os.close(read_fd) os.close(write_fd) if __name__ == "__main__": unittest.main()