282 lines
12 KiB
Python
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())
|