124 lines
5.9 KiB
Python
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()
|