Files

124 lines
5.9 KiB
Python

"""Main-menu regressions without speech, GUI, audio, or configuration services.
Run from the library root: python -m unittest discover -s tests -v
"""
import importlib.util
from pathlib import Path
import sys
from types import ModuleType, SimpleNamespace
import unittest
from unittest.mock import Mock, patch
import pygame
def load_menu():
"""Load the real menu code with isolated framework dependencies."""
packageName = "_menu_test_library"
package = ModuleType(packageName)
package.__path__ = [str(Path(__file__).resolve().parents[1])]
modules = {packageName: package}
exports = {
"speech": ["messagebox", "Speech"],
"sound": ["adjust_master_volume", "adjust_bgm_volume", "adjust_sfx_volume", "play_bgm"],
"display": ["display_text"],
"scoreboard": ["Scoreboard"],
"services": ["PathService", "ConfigService"],
}
for moduleName, names in exports.items():
dependency = ModuleType(f"{packageName}.{moduleName}")
for name in names:
setattr(dependency, name, Mock())
modules[dependency.__name__] = dependency
spec = importlib.util.spec_from_file_location(f"{packageName}.menu", Path(package.__path__[0]) / "menu.py")
menu = importlib.util.module_from_spec(spec)
with patch.dict(sys.modules, modules):
spec.loader.exec_module(menu)
return menu
class MenuTests(unittest.TestCase):
def make_menu(self, keyCodes, highScores=True, files=True):
menu = load_menu()
menu.Scoreboard.has_high_scores.return_value = highScores
menu.pygame = Mock()
for name in dir(pygame):
if name.startswith("K_") or name in ["KEYDOWN", "QUIT", "KMOD_ALT"]:
setattr(menu.pygame, name, getattr(pygame, name))
menu.pygame.key.get_mods.return_value = 0
menu.pygame.mixer.music.get_busy.return_value = True
menu.pygame.event.wait.side_effect = [
SimpleNamespace(type=pygame.KEYDOWN, key=keyCode) for keyCode in keyCodes
]
menu.time = Mock()
pathPatch = patch.object(menu.os.path, "isfile", return_value=files)
pathPatch.start()
self.addCleanup(pathPatch.stop)
return menu
def spoken_labels(self, menu):
return [call.args[0] for call in menu.Speech.get_instance.return_value.speak.call_args_list]
def test_custom_action_is_spoken_readably_and_returned_unchanged(self):
menu = self.make_menu([pygame.K_DOWN, pygame.K_DOWN, pygame.K_RETURN])
result = menu.game_menu({}, None, "load_game")
self.assertEqual(result, "load_game")
self.assertEqual(self.spoken_labels(menu), ["play", "high scores", "load game"])
def test_standard_actions_still_dispatch_to_their_handlers(self):
options = ["play", "high_scores", "load_game", "learn_sounds", "instructions", "credits", "donate", "exit_game"]
for actionName in ["high_scores", "learn_sounds", "instructions", "credits", "donate"]:
with self.subTest(actionName=actionName):
keyCodes = [pygame.K_DOWN] * options.index(actionName) + [pygame.K_RETURN, pygame.K_HOME, pygame.K_RETURN]
menu = self.make_menu(keyCodes)
for handlerName in ["learn_sounds", "instructions", "credits", "donate"]:
setattr(menu, handlerName, Mock())
sounds = {}
self.assertEqual(menu.game_menu(sounds, None, "load_game"), "play")
self.assertIn(actionName.replace("_", " "), self.spoken_labels(menu))
if actionName == "high_scores":
menu.Scoreboard.display_high_scores.assert_called_once_with()
elif actionName == "learn_sounds":
menu.learn_sounds.assert_called_once_with(sounds)
else:
getattr(menu, actionName).assert_called_once_with()
def test_navigation_home_end_and_arrows_keep_the_same_order(self):
menu = self.make_menu([pygame.K_DOWN, pygame.K_DOWN, pygame.K_UP, pygame.K_END, pygame.K_HOME, pygame.K_RETURN])
self.assertEqual(menu.game_menu({}, None, "load_game"), "play")
self.assertEqual(self.spoken_labels(menu), ["play", "high scores", "load game", "high scores", "exit game", "play"])
def test_exit_action_and_escape_keep_exit_behavior(self):
for keyCodes in [[pygame.K_END, pygame.K_RETURN], [pygame.K_ESCAPE]]:
with self.subTest(keyCodes=keyCodes):
menu = self.make_menu(keyCodes)
menu.exit_game = Mock(side_effect=SystemExit)
with self.assertRaises(SystemExit):
menu.game_menu({})
menu.exit_game.assert_called_once_with(500)
if keyCodes[0] == pygame.K_END:
self.assertIn("exit game", self.spoken_labels(menu))
def test_play_callback_and_custom_return_keep_original_contract(self):
menu = self.make_menu([pygame.K_RETURN, pygame.K_DOWN, pygame.K_RETURN], highScores=False, files=False)
playCallback = Mock()
self.assertEqual(menu.game_menu({}, playCallback, "exit_level"), "exit_level")
playCallback.assert_called_once_with()
self.assertIn("exit level", self.spoken_labels(menu))
def test_custom_labels_keep_existing_case_and_punctuation(self):
menu = self.make_menu([pygame.K_DOWN, pygame.K_RETURN], highScores=False, files=False)
label = "About My Game!"
self.assertEqual(menu.game_menu({}, None, label), label)
self.assertEqual(self.spoken_labels(menu), ["play", label])
def test_generic_instruction_menu_does_not_rewrite_option_text(self):
menu = self.make_menu([pygame.K_DOWN, pygame.K_RETURN])
self.assertEqual(menu.instruction_menu({}, "Select a file:", "example_file.json"), "example_file.json")
self.assertEqual(self.spoken_labels(menu), ["Select a file:", "example_file.json"])
if __name__ == "__main__":
unittest.main()