slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+254
View File
@@ -0,0 +1,254 @@
"""End-to-End-Smoke-Tests im echten Browser (Playwright, headless).
Die Tests starten die Flask-App selbst (temporäre Datenbank, zufälliger Port) und
verwenden standardmäßig ein installiertes Microsoft Edge oder Google Chrome. Mit
``GLORY_E2E_BROWSER`` lässt sich ein Kanal erzwingen (``msedge``, ``chrome`` oder
``chromium`` für den von Playwright geladenen Browser). Ist kein Browser verfügbar,
werden die Tests mit Begründung übersprungen.
"""
from __future__ import annotations
import os
import threading
import time
from collections.abc import Iterator
from pathlib import Path
import pytest
from app import create_app
sync_api = pytest.importorskip("playwright.sync_api")
expect = sync_api.expect
pytestmark = pytest.mark.e2e
GAME = "window.gloryDefense"
def _channels() -> list[str | None]:
forced = os.environ.get("GLORY_E2E_BROWSER", "").strip()
if forced:
return [None if forced == "chromium" else forced]
return ["msedge", "chrome", None]
@pytest.fixture(scope="module")
def live_server(tmp_path_factory: pytest.TempPathFactory) -> Iterator[str]:
from werkzeug.serving import make_server
db_path: Path = tmp_path_factory.mktemp("e2e") / "e2e.sqlite3"
app = create_app({"TESTING": True, "SECRET_KEY": "e2e-secret", "DATABASE_PATH": str(db_path)})
server = make_server("127.0.0.1", 0, app, threaded=True)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
yield f"http://127.0.0.1:{server.server_port}"
server.shutdown()
thread.join(timeout=5)
@pytest.fixture(scope="module")
def browser() -> Iterator[object]:
with sync_api.sync_playwright() as playwright:
failures: list[str] = []
for channel in _channels():
try:
launched = playwright.chromium.launch(channel=channel, headless=True)
except Exception as exc: # jeder Startfehler heißt: nächsten Kandidaten probieren
failures.append(f"{channel or 'chromium'}: {str(exc).splitlines()[0]}")
continue
yield launched
launched.close()
return
pytest.skip("Kein Browser für Playwright gefunden – " + " | ".join(failures))
@pytest.fixture()
def page(browser: object, live_server: str) -> Iterator[object]:
context = browser.new_context(viewport={"width": 1280, "height": 720})
page = context.new_page()
page.errors = [] # type: ignore[attr-defined]
def on_console(msg: object) -> None:
if msg.type == "error":
page.errors.append(f"console.{msg.type}: {msg.text}")
page.on("pageerror", lambda exc: page.errors.append(f"pageerror: {exc}"))
page.on("console", on_console)
page.goto(live_server + "/")
yield page
context.close()
def _wait_until(page: object, expression: str, timeout_s: float = 15.0) -> None:
"""Wartet, bis ``expression`` im Browser wahr ist.
Absichtlich nicht ``page.wait_for_function``: Das wertet den Ausdruck per ``eval``
aus, was die Content-Security-Policy der App (kein ``unsafe-eval``) zu Recht
blockiert. ``page.evaluate`` läuft über das DevTools-Protokoll und ist davon
nicht betroffen – so testen wir mit der echten, unveränderten CSP.
"""
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
if page.evaluate(f"() => Boolean({expression})"):
return
page.wait_for_timeout(100)
raise AssertionError(f"Zeitüberschreitung beim Warten auf: {expression}")
def _wait_for_scene(page: object, key: str) -> None:
_wait_until(page, f"{GAME} && {GAME}.scene.isActive('{key}')")
def _accept_disclaimer_and_start(page: object) -> None:
accept = page.get_by_test_id("disclaimer-accept")
expect(accept).to_be_visible()
accept.click()
_wait_for_scene(page, "Menu")
page.locator("#game canvas").click(position={"x": 40, "y": 40})
page.keyboard.press("Enter")
_wait_for_scene(page, "Briefing")
page.keyboard.press("Space")
page.keyboard.press("Space")
_wait_for_scene(page, "Game")
_wait_for_scene(page, "Hud")
def test_page_loads_disclaimer_then_game_starts(page: object) -> None:
expect(page).to_have_title("Glory Defense")
_accept_disclaimer_and_start(page)
assert page.evaluate("localStorage.getItem('gloryDefense.disclaimerAccepted.v1')") == "true"
# Etwas spielen: nach rechts laufen, zielen und Dauerfeuer geben.
page.keyboard.down("d")
page.mouse.move(1000, 300)
page.mouse.down()
page.wait_for_timeout(1200)
page.mouse.up()
page.keyboard.up("d")
_wait_until(page, f"{GAME}.scene.getScene('Game').gameTime > 1500")
_wait_until(page, f"{GAME}.scene.getScene('Game').runToken !== null")
state = page.evaluate(
f"""() => {{
const s = {GAME}.scene.getScene('Game');
return {{ magazine: s.player.weapon.magazine, x: s.player.x }};
}}"""
)
assert state["magazine"] < 30, "Es wurde nicht geschossen"
assert state["x"] > 1200, "Der Präsident hat sich nicht bewegt"
# Pause per Esc, Lautstärke ändern, weiterspielen.
page.keyboard.press("Escape")
resume = page.get_by_test_id("pause-resume")
expect(resume).to_be_visible()
page.get_by_test_id("volume-music").fill("40")
resume.click()
_wait_for_scene(page, "Game")
assert page.errors == [] # type: ignore[attr-defined]
def test_orbital_commands_via_keyboard(page: object) -> None:
_accept_disclaimer_and_start(page)
page.mouse.move(800, 360)
# Orbitalschlag mit Pfeiltasten, Nachschub mit WASD – beides bei gehaltenem Q.
page.keyboard.down("q")
for key in ("ArrowUp", "ArrowRight", "ArrowDown", "ArrowDown"):
page.keyboard.press(key)
for key in ("a", "a", "s", "d"):
page.keyboard.press(key)
page.keyboard.up("q")
_wait_until(page, f"{GAME}.scene.getScene('Game').orbital.pending.length === 2", timeout_s=5)
ready = page.evaluate(
f"""() => {{
const s = {GAME}.scene.getScene('Game');
return s.orbital.hudState(s.gameTime).commands.map(c => [c.id, c.ready]);
}}"""
)
assert ready == [["strike", False], ["supply", False], ["turret", True]]
# Nach dem Einschlag liegt eine Nachschubkapsel in der Arena.
_wait_until(page, f"{GAME}.scene.getScene('Game').pods.countActive(true) === 1", timeout_s=8)
assert page.errors == [] # type: ignore[attr-defined]
def test_disclaimer_is_remembered_and_reachable_from_menu(page: object) -> None:
page.get_by_test_id("disclaimer-accept").click()
_wait_for_scene(page, "Menu")
page.reload()
_wait_for_scene(page, "Menu")
expect(page.locator("#disclaimer-overlay")).to_have_count(0)
# Im Menü: 4. Eintrag „Disclaimer“ (dreimal nach unten, Enter).
page.locator("#game canvas").click(position={"x": 40, "y": 40})
for _ in range(3):
page.keyboard.press("ArrowDown")
page.keyboard.press("Enter")
expect(page.locator("#disclaimer-overlay")).to_be_visible()
expect(page.locator("#disclaimer-overlay")).to_contain_text("Satire und reine Fiktion")
page.get_by_test_id("disclaimer-accept").click()
expect(page.locator("#disclaimer-overlay")).to_have_count(0)
assert page.errors == [] # type: ignore[attr-defined]
def test_game_over_submits_highscore(page: object) -> None:
_accept_disclaimer_and_start(page)
_wait_until(page, f"{GAME}.scene.getScene('Game').runToken !== null")
_wait_until(page, f"{GAME}.scene.getScene('Game').gameTime > 2000")
# Test-Abkürzung: Punkte gutschreiben und den Präsidenten fallen lassen.
page.evaluate(
f"""() => {{
const s = {GAME}.scene.getScene('Game');
s.score.addBonus(150);
s.player.hitInvulnerableUntil = 0;
s.player.dodgeUntil = 0;
s.damagePlayer(9999);
}}"""
)
_wait_for_scene(page, "GameOver")
name = page.get_by_test_id("highscore-name")
expect(name).to_be_visible()
name.fill("E2E Kadett")
page.get_by_test_id("highscore-submit").click()
expect(page.locator(".highscore-list__own")).to_contain_text("E2E Kadett")
expect(page.locator("#gameover-overlay")).to_contain_text("Platz 1")
assert page.errors == [] # type: ignore[attr-defined]
def test_game_runs_with_placeholders_when_assets_are_missing(
browser: object, live_server: str
) -> None:
"""Abnahmekriterium: Das Spiel ist auch ohne echte Grafiken vollständig spielbar."""
context = browser.new_context(viewport={"width": 1280, "height": 720})
page = context.new_page()
page.errors = [] # type: ignore[attr-defined]
page.on("pageerror", lambda exc: page.errors.append(str(exc)))
# Sämtliche Grafik-, Schrift- und Klangdateien blockieren.
page.route("**/static/assets/**", lambda route: route.abort())
page.goto(live_server + "/")
_accept_disclaimer_and_start(page)
_wait_until(page, f"{GAME}.scene.getScene('Game').gameTime > 2500")
textures = page.evaluate(
f"""() => {{
const t = {GAME}.textures;
return ['player', 'enemy-krabbler', 'enemy-walzmeister', 'supply-pod', 'emblem']
.map(key => [key, t.exists(key), t.get(key).source[0].width]);
}}"""
)
# Platzhalter existieren (Größen der Platzhalter, nicht der SVGs).
assert all(exists for _key, exists, _width in textures)
assert dict((key, width) for key, _e, width in textures)["player"] != 64
assert page.errors == [] # type: ignore[attr-defined]
context.close()
def test_disclaimer_page_works_without_javascript(browser: object, live_server: str) -> None:
context = browser.new_context(java_script_enabled=False)
page = context.new_page()
page.goto(live_server + "/disclaimer")
expect(page.locator("main")).to_contain_text("frei erfunden")
expect(page.get_by_role("link", name="Zurück zum Spiel")).to_be_visible()
context.close()