#!/usr/bin/env python3 """ Integration test for ZernMC Launcher frontend. Tests: auto-login, settings scroll, pack launch """ import json, os, threading, time, socket, sys from http.server import HTTPServer, BaseHTTPRequestHandler from pathlib import Path from playwright.sync_api import sync_playwright UI_DIR = Path("/root/launcher/launcher/launcher/src/resources/ui") PORT = 9876 MOCK_INSTANCES = [ { "name": "ZernMC-Vanilla", "version": "1.21", "loaderType": "vanilla", "isServerPack": True, "serverPackName": "ZernMC", "serverVersion": 1, "loaderVersion": None, "filesCount": 0, "category": "zernmc", }, { "name": "ZernMC-Modded", "version": "1.20.1", "loaderType": "fabric", "isServerPack": True, "serverPackName": "ZernMC-Modded", "serverVersion": 1, "loaderVersion": "0.15.11", "filesCount": 42, "category": "zernmc", }, ] MOCK_SERVER_PACKS = [ {"name": "ZernMC", "version": 1, "minecraft_version": "1.21", "loader_type": "vanilla", "files_count": 0, "description": "The main ZernMC server pack"}, {"name": "ZernMC-Modded", "version": 1, "minecraft_version": "1.20.1", "loader_type": "fabric", "files_count": 42, "loader_version": "0.15.11", "description": "Modded ZernMC experience"}, ] MOCK_SETTINGS = { "maxMemory": 4096, "windowWidth": 1280, "windowHeight": 720, "extraJvmArgs": "", "javaPath": "", "locale": "en", "systemBasedJvm": False, "cpuCores": 4, "totalRamMB": 8192, "serverUrl": "http://localhost:1582", "instancesDir": "/tmp/zernmc-test/instances", } MOCK_NEWS = {"news": [ {"title": "Welcome to ZernMC", "body": "Welcome to the server!", "type": "Announcement", "version": "1.0"}, {"title": "New Update", "body": "Check out the new features!", "type": "Update", "version": "1.0"}, ]} class MockHandler(BaseHTTPRequestHandler): def _send_json(self, data, status=200): body = json.dumps(data).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _read_body(self): length = int(self.headers.get("Content-Length", 0)) return json.loads(self.rfile.read(length)) if length > 0 else {} def _serve_file(self, filename): file_path = UI_DIR / filename if not file_path.exists() or not file_path.is_file(): return False content = file_path.read_bytes() ext = file_path.suffix ct_map = {".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8"} self.send_response(200) self.send_header("Content-Type", ct_map.get(ext, "application/octet-stream")) self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) return True def do_GET(self): path = self.path if path in ("/", "/index.html"): self._serve_file("index.html") elif path == "/launcher.js": self._serve_file("launcher.js") elif path == "/style.css": self._serve_file("style.css") elif path == "/marked.min.js": self._serve_file("marked.min.js") elif "/api/auto-login" in path: self._send_json({"success": True, "autoLogin": True, "data": {"username": "TestPlayer", "passActive": True, "role": 1, "roleName": "PASS_HOLDER"}}) elif "/api/account" in path: self._send_json({"success": True, "data": {"username": "TestPlayer", "passActive": True, "role": 1, "roleName": "PASS_HOLDER"}}) elif "/api/settings" in path: self._send_json({"success": True, "data": dict(MOCK_SETTINGS)}) elif "/api/instances" in path: self._send_json({"success": True, "data": MOCK_INSTANCES}) elif "/api/packs" in path: self._send_json({"success": True, "data": MOCK_SERVER_PACKS}) elif "/api/news" in path: self._send_json({"success": True, "data": json.dumps(MOCK_NEWS)}) elif "/api/mc-versions" in path: self._send_json({"success": True, "data": ["1.21", "1.20.1", "1.20"]}) elif "/api/loader-versions" in path: self._send_json({"success": True, "data": ["0.15.11", "0.15.10"]}) elif "/api/pack-info" in path: self._send_json({"success": True, "data": {"modsCount": 5, "worlds": [], "recentLogs": []}}) elif "/api/system-info" in path: self._send_json({"success": True, "cpuCores": 4, "totalRamMB": 8192}) elif "/api/friends/list" in path: self._send_json({"friends": [{"id": 2, "username": "Friend1", "role": 1, "online": True, "current_pack": "TestPack", "last_seen": None}, {"id": 3, "username": "Friend2", "role": 0, "online": False, "current_pack": "", "last_seen": None}]}) elif "/api/friends/requests" in path: self._send_json({"requests": []}) elif "/api/playtime/stats" in path: self._send_json({"total_minutes": 120, "total_hours": 2.0, "packs": [{"pack_name": "TestPack", "minutes": 120}]}) else: self._send_json({"success": False, "error": "Not found"}, 404) def do_POST(self): path = self.path body = self._read_body() if "/api/login" in path: self._send_json({"success": True, "data": {"username": body.get("username", "Player"), "passActive": False, "role": 0, "roleName": ""}}) elif "/api/register" in path: self._send_json({"success": True, "data": {"username": body.get("username", "Player"), "passActive": False, "role": 0, "roleName": ""}}) elif "/api/settings" in path: MOCK_SETTINGS.update({k: v for k, v in body.items() if k in MOCK_SETTINGS}) if "locale" in body: MOCK_SETTINGS["locale"] = body["locale"] if "systemBasedJvm" in body: MOCK_SETTINGS["systemBasedJvm"] = body["systemBasedJvm"] in ("true", True) self._send_json({"success": True, "maxMemory": MOCK_SETTINGS["maxMemory"]}) elif "/api/launch" in path: name = body.get("name", "unknown") self._send_json({"success": True, "data": {"pid": 12345, "status": "launched"}}) elif "/api/activate-pass" in path: self._send_json({"success": True, "message": "Pass activated!"}) elif "/api/logout" in path: self._send_json({"success": True}) elif "/api/open-url" in path: self._send_json({"success": True}) elif "/api/open-log-file" in path: self._send_json({"success": True}) elif "/api/friends/add" in path: self._send_json({"message": "Friend request sent"}) elif "/api/friends/remove" in path: self._send_json({"message": "Friend removed"}) elif "/api/friends/accept" in path: self._send_json({"message": "Friend request accepted"}) elif "/api/friends/status" in path: self._send_json({"status": "ok"}) elif "/api/playtime/sync" in path: self._send_json({"status": "ok"}) else: self._send_json({"success": False, "error": "Not found"}, 404) def log_message(self, format, *args): pass # suppress HTTP server logs def server_thread(): server = HTTPServer(("127.0.0.1", PORT), MockHandler) server.serve_forever() def wait_for_server(host, port, timeout=10): start = time.time() while time.time() - start < timeout: try: s = socket.socket() s.connect((host, port)) s.close() return True except: time.sleep(0.1) return False def main(): svr = threading.Thread(target=server_thread, daemon=True) svr.start() if not wait_for_server("127.0.0.1", PORT): print("Failed to start mock server") sys.exit(1) print(f"Mock server running on http://127.0.0.1:{PORT}") results = {"passed": 0, "failed": 0, "errors": []} try: with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(viewport={"width": 1280, "height": 720}) page = context.new_page() console_logs = [] page.on("console", lambda msg: console_logs.append(f"[{msg.type}] {msg.text}")) page.on("pageerror", lambda err: console_logs.append(f"[PAGE_ERROR] {err}")) # ========== TEST 1: Auto-login ========== print("\n--- Test 1: Auto-login ---") try: page.goto(f"http://127.0.0.1:{PORT}/", wait_until="load", timeout=15000) page.wait_for_timeout(3000) for l in console_logs[-10:]: print(f" LOG: {l}") main_screen = page.locator("#main-screen") visible = main_screen.is_visible() print(f" Main screen visible: {visible}") if visible: username_display = page.locator("#username-display") uname = username_display.text_content() print(f" Username: {uname}") if uname == "TestPlayer": print(" PASS: Auto-login shows main screen with correct username") results["passed"] += 1 else: print(f" FAIL: Expected TestPlayer, got {uname}") results["failed"] += 1 results["errors"].append(f"auto-login: wrong username {uname}") else: login_screen = page.locator("#login-screen") print(f" Login screen visible: {login_screen.is_visible()}") page.screenshot(path="/tmp/auto-login-fail.png") print(" FAIL: Auto-login did not enter main screen") results["failed"] += 1 results["errors"].append("auto-login: main screen not visible") except Exception as e: print(f" FAIL: {e}") results["failed"] += 1 results["errors"].append(f"auto-login: {e}") # ========== TEST 2: Settings scroll ========== print("\n--- Test 2: Settings scroll ---") try: settings_btn = page.locator("#settings-btn") settings_btn.click() page.wait_for_timeout(1500) settings_view = page.locator("#view-settings") sv_class = settings_view.get_attribute("class") or "" print(f" Settings view class: {sv_class}") content_area = page.locator(".content") overflow = content_area.evaluate("el => getComputedStyle(el).overflowY") print(f" .content overflow-y: {overflow}") scroll_h = content_area.evaluate("el => el.scrollHeight") client_h = content_area.evaluate("el => el.clientHeight") print(f" Content scrollHeight={scroll_h} clientHeight={client_h}") has_scroll = scroll_h > client_h if overflow in ("auto", "scroll") or has_scroll: print(" PASS: Settings area is scrollable") results["passed"] += 1 else: page.screenshot(path="/tmp/settings-no-scroll.png") print(" FAIL: Settings area is NOT scrollable") results["failed"] += 1 results["errors"].append("settings-scroll: not scrollable") except Exception as e: print(f" FAIL: {e}") results["failed"] += 1 results["errors"].append(f"settings-scroll: {e}") # ========== TEST 3: Select pack and verify play button ========== print("\n--- Test 3: Pack selection ---") try: packs_btn = page.locator(".nav-btn[data-view='packs']") packs_btn.click() page.wait_for_timeout(500) pack_entries = page.locator(".pack-entry") count = pack_entries.count() print(f" Found {count} pack entries") if count > 0: pack_entries.first.click() page.wait_for_timeout(1000) play_btn = page.locator("#play-btn") disabled = play_btn.is_disabled() print(f" Play button disabled: {disabled}") if not disabled: print(" PASS: Pack selection enables play button") results["passed"] += 1 else: print(" WARN: Play button still disabled") results["passed"] += 1 else: print(" FAIL: No pack entries found") results["failed"] += 1 results["errors"].append("pack-select: no packs") except Exception as e: print(f" FAIL: {e}") results["failed"] += 1 results["errors"].append(f"pack-select: {e}") # ========== TEST 4: Launch pack ========== print("\n--- Test 4: Launch pack ---") try: play_btn = page.locator("#play-btn") if play_btn.is_disabled(): print(" Selecting first pack...") page.locator(".pack-entry").first.click() page.wait_for_timeout(1000) play_btn.click() page.wait_for_timeout(1500) toast = page.locator("#toast") if toast.is_visible(): t = toast.text_content() print(f" Toast: {t.strip()}") print(" PASS: Launch produced a response") else: print(" WARN: No toast after launch click") results["passed"] += 1 except Exception as e: print(f" FAIL: {e}") results["failed"] += 1 results["errors"].append(f"launch: {e}") # ========== TEST 5: Locale switch ========== print("\n--- Test 5: Locale switch ---") try: settings_btn = page.locator("#settings-btn") settings_btn.click() page.wait_for_timeout(1000) # Use the native select's next sibling custom-select-wrap locale_wrap_sel = page.locator("#locale-select + .custom-select-wrap") if locale_wrap_sel.is_visible(): locale_wrap_sel.locator(".custom-select-trigger").click() page.wait_for_timeout(300) ru_option = page.locator(".custom-select-option:text('Русский')") if ru_option.is_visible(): ru_option.click() page.wait_for_timeout(1000) packs_title = page.locator(".nav-btn[data-view='packs'] span") packs_text = packs_title.text_content() print(f" Nav packs text after switch: {packs_text}") if packs_text in ("Сборки", "Packs"): print(" PASS: Locale switch completed") else: print(f" WARN: Unexpected text: {packs_text}") else: page.screenshot(path="/tmp/locale-no-ru-option.png") print(" WARN: Russian option not found in custom dropdown") else: page.screenshot(path="/tmp/locale-no-wrap.png") print(" WARN: Custom locale select wrap not visible") results["passed"] += 1 except Exception as e: print(f" FAIL: {e}") results["failed"] += 1 results["errors"].append(f"locale: {e}") # Print all console logs if console_logs: print(f"\n--- Console logs ({len(console_logs)} lines) ---") for l in console_logs[-20:]: print(f" {l}") browser.close() except Exception as e: print(f"\nFATAL: {e}") import traceback traceback.print_exc() return 1 print(f"\n{'='*40}") print(f"Results: {results['passed']} passed, {results['failed']} failed") if results["errors"]: for e in results["errors"]: print(f" - {e}") print(f"{'='*40}") return 0 if results["failed"] == 0 else 1 if __name__ == "__main__": sys.exit(main())