"""Einstiegspunkte (``python -m sandbox``, CLI) und Randfälle der Eingabeverarbeitung.""" import runpy import sys from dataclasses import replace from pathlib import Path import pygame import pytest from sandbox import main as main_module from sandbox.config import APP, INTERACTION, Theme, UiConfig from sandbox.main import App, FrameStats, main from sandbox.ui.style import UiStyle, _load_font from tests.app.driver import click, frames, key, move, post, press, release, screen_pos def test_python_dash_m_starts_the_app(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: screenshot = tmp_path / "module.png" monkeypatch.setattr( sys, "argv", ["sandbox", "--screenshot", str(screenshot), "--seconds", "0.1"] ) with pytest.raises(SystemExit) as exit_info: runpy.run_module("sandbox", run_name="__main__") assert exit_info.value.code == 0 assert screenshot.exists() def test_main_without_options_runs_the_interactive_loop(monkeypatch: pytest.MonkeyPatch) -> None: calls: list[float | None] = [] monkeypatch.setattr( main_module.App, "run", lambda self, max_seconds=None: calls.append(max_seconds) ) assert main([]) == 0 assert calls == [None] def test_main_loop_ends_when_the_window_is_closed(app: App) -> None: post(pygame.QUIT) app.run() # kehrt nach dem ersten Frame zurück assert not app.running def test_frame_stats_are_smoothed_and_ignore_empty_frames() -> None: stats = FrameStats(smoothing=0.5) stats.update(1 / 50, 0.002) assert stats.fps == pytest.approx(50) stats.update(1 / 100, 0.004) assert stats.fps == pytest.approx(75) stats.update(0.0, 0.0) # z. B. allererster Frame: keine Division durch 0 assert stats.fps == pytest.approx(75) assert stats.physics_ms > 0 def test_clicks_outside_the_world_without_sidebar_are_ignored(app: App) -> None: app.input.sidebar = None # InputHandler auch ohne Sidebar nutzbar click(app, (10, 10)) # links neben der Welt assert app.world.bodies == [] click(app, screen_pos(app, 400, 300)) assert len(app.world.bodies) == 1 def test_middle_click_and_delete_without_target_do_nothing(app: App) -> None: click(app, screen_pos(app, 400, 300), pygame.BUTTON_MIDDLE) assert app.world.bodies == [] move(app, screen_pos(app, 600, 300)) key(app, "delete") # nichts unter der Maus click(app, screen_pos(app, 600, 300), pygame.BUTTON_RIGHT) assert app.world.bodies == [] def test_spawn_drag_is_ignored_when_the_world_is_full(app: App) -> None: app.close() full = App(replace(APP, interaction=replace(INTERACTION, max_bodies=1))) try: click(full, screen_pos(full, 400, 300)) tile = full.sidebar.widget("spawn_tile").rect press(full, tile.center) move(full, screen_pos(full, 200, 200)) assert len(full.world.bodies) == 1 assert full.drag.body is None assert full.input.spawn_preview() is None # voll: keine Vorschau mehr release(full, screen_pos(full, 200, 200)) frames(full) finally: full.close() def test_unknown_widget_names_raise(app: App) -> None: with pytest.raises(KeyError): app.sidebar.widget("gibt-es-nicht") def test_missing_system_fonts_fall_back_to_the_builtin_font() -> None: pygame.font.init() try: font = _load_font(("keine-solche-schrift-xyz",), 14, bold=True) assert font.bold assert font.size("Test")[0] > 0 finally: pygame.font.quit() def test_style_initializes_the_font_module_itself() -> None: pygame.font.quit() style = UiStyle(Theme(), UiConfig()) try: assert pygame.font.get_init() assert style.render_text("x", style.label_font, (0, 0, 0)).get_width() > 0 finally: pygame.font.quit()