Files
launcher/test_install_modal.py
T

282 lines
12 KiB
Python

#!/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())