#!/usr/bin/env python3 """ Playwright test for the launcher install modal. Mocks the server and tests that the install modal loads packs correctly. """ import json, os, threading, time, socket, sys from http.server import HTTPServer, BaseHTTPRequestHandler from pathlib import Path from urllib.parse import urlparse, parse_qs from playwright.sync_api import sync_playwright UI_DIR = Path("/root/launcher/launcher/launcher/src/resources/ui") PORT = 9877 MOCK_INSTANCES = [ {"name": "ZernMC-Vanilla", "version": "1.21", "loaderType": "vanilla", "isServerPack": True, "serverPackName": "ZernMC", "serverVersion": 1, "filesCount": 0, "category": "zernmc"}, {"name": "My Local World", "version": "1.20.1", "loaderType": "fabric", "isServerPack": False, "serverPackName": None, "serverVersion": 0, "filesCount": 42, "category": "local", "loaderVersion": "0.15.11"}, ] MOCK_SERVER_PACKS = [ {"name": "ZernMC", "version": 5, "minecraft_version": "1.21", "loader_type": "vanilla", "files_count": 128, "description": "Main server pack", "displayName": "ZernMC"}, {"name": "ZernMC-Modded", "version": 3, "minecraft_version": "1.20.1", "loader_type": "fabric", "files_count": 256, "loader_version": "0.15.11", "description": "Modded pack"}, ] MOCK_SETTINGS = { "maxMemory": 4096, "windowWidth": 1280, "windowHeight": 720, "extraJvmArgs": "", "javaPath": "", "locale": "en", "systemBasedJvm": False, "cpuCores": 4, "totalRamMB": 8192, "serverUrl": "http://localhost:9877", "instancesDir": "/tmp/zernmc-test/instances", } 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({"news": []})}) elif "/api/mc-versions" in path: self._send_json({"success": True, "data": ["1.21", "1.20.1", "1.20", "1.19.4"]}) 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/check-updates" in path: qs = parse_qs(urlparse(self.path).query) name = qs.get("name", [""])[0] for inst in MOCK_INSTANCES: if inst["name"] == name: self._send_json({"success": True, "data": {"hasUpdate": False, "serverVersion": 1, "localVersion": 1}}) return self._send_json({"success": False, "error": "Instance not found"}, 404) 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": []}) elif "/api/friends/requests" in path: self._send_json({"requests": []}) elif "/api/playtime/stats" in path: self._send_json({"total_minutes": 0, "total_hours": 0, "packs": []}) 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: self._send_json({"success": True, "maxMemory": MOCK_SETTINGS["maxMemory"]}) elif "/api/launch" in path: 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/update" in path: name = body.get("name", "") self._send_json({"success": True, "data": {"version": 2}}) elif "/api/install" in path: self._send_json({"success": True, "message": "Installation started"}) elif "/api/install/progress" in path: self._send_json({"success": True, "data": { "label": "Complete", "inProgress": False, "percent": 100, "stageName": "Done", "stageIndex": 0, "stageCount": 1 }}) else: self._send_json({"success": False, "error": "Not found"}, 404) def log_message(self, format, *args): pass def run_server(): 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=run_server, daemon=True) svr.start() if not wait_for_server("127.0.0.1", PORT): print("Failed to start mock server") return 1 print(f"Mock server running on http://127.0.0.1:{PORT}") passed = 0 failed = 0 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_errors = [] page.on("console", lambda msg: console_errors.append(f"[{msg.type}] {msg.text}") if msg.type == "error" else None) page.on("pageerror", lambda err: console_errors.append(f"[PAGE_ERROR] {err}")) try: # ===== Navigate and wait for login ===== page.goto(f"http://127.0.0.1:{PORT}/", wait_until="load", timeout=15000) page.wait_for_timeout(3000) # Verify we're on main screen main_screen = page.locator("#main-screen") if not main_screen.is_visible(): print("FAIL: Main screen not visible after auto-login") return 1 print("--- Test 1: Open install modal ---") add_pack_btn = page.locator("#add-pack-btn") add_pack_btn.click() page.wait_for_timeout(1000) install_modal = page.locator("#install-modal") if install_modal.is_visible(): print(" PASS: Install modal opened") passed += 1 else: print(" FAIL: Install modal not visible") failed += 1 # ===== Check pack select (zernmc-pack-select) ===== print("--- Test 2: Pack select populated ---") pack_select = page.locator("#zernmc-pack-select") page.wait_for_timeout(500) options_text = pack_select.evaluate("sel => Array.from(sel.options).map(o => o.text).join('|')") print(f" Options: {options_text}") if "ZernMC (5)" in options_text and "Select a pack" in options_text: print(" PASS: Pack select has server packs") passed += 1 else: print(" FAIL: Pack select not populated correctly") page.screenshot(path="/tmp/install-modal-packs.png") failed += 1 # ===== Check MC version select (may not exist in HTML) ===== print("--- Test 3: MC version select handled gracefully ---") mc_select = page.locator("#mc-version-select") mc_exists = mc_select.count() if mc_exists == 0: print(" PASS: MC version select absent (custom tab disabled), no crash") else: options_text = mc_select.evaluate("sel => Array.from(sel.options).map(o => o.text).join('|')") print(f" Options: {options_text}") if "1.21" in options_text: print(" PASS: MC version select populated") else: print(" FAIL: MC version select not populated") page.screenshot(path="/tmp/install-modal-mc.png") passed += 1 # ===== Check install button exists ===== print("--- Test 4: Install button exists ---") install_btn = page.locator("#install-zernmc-btn") if install_btn.is_visible(): print(" PASS: Install button visible") passed += 1 else: print(" FAIL: Install button not visible") failed += 1 # ===== Check instance name input ===== print("--- Test 5: Instance name input exists ---") name_input = page.locator("#zernmc-instance-name") if name_input.is_visible(): print(" PASS: Instance name input visible") passed += 1 else: print(" FAIL: Instance name input not visible") failed += 1 except Exception as e: print(f" FAIL: {e}") import traceback traceback.print_exc() failed += 1 if console_errors: print(f"\n--- Console errors ({len(console_errors)}) ---") for e in console_errors[-10:]: print(f" {e}") browser.close() print(f"\n{'='*40}") print(f"Results: {passed} passed, {failed} failed") print(f"{'='*40}") return 0 if failed == 0 else 1 if __name__ == "__main__": sys.exit(main())