383 lines
17 KiB
Python
383 lines
17 KiB
Python
#!/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())
|