1360 lines
71 KiB
Python
1360 lines
71 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Autonomous Automated Test Runner for Plants vs. Zombies (ai.qpid.pro)
|
||
Tests static integrity, HTTP/HTTPS endpoints, JS scripts, and PvZ mechanics.
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import re
|
||
import urllib.request
|
||
import ssl
|
||
import json
|
||
|
||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
sys.path.insert(0, os.path.join(BASE_DIR, "scripts"))
|
||
import db
|
||
GAME_DIR = os.path.join(BASE_DIR, "game")
|
||
|
||
TEST_RESULTS = []
|
||
|
||
def record(name, status, detail=""):
|
||
TEST_RESULTS.append({"name": name, "status": status, "detail": detail})
|
||
symbol = "✅ PASS" if status == "PASS" else "❌ FAIL"
|
||
print(f"[{symbol}] {name}" + (f": {detail}" if detail else ""))
|
||
|
||
def test_file_structure():
|
||
print("\n--- 1. File Structure & Asset Verification ---")
|
||
required_files = [
|
||
"index.html",
|
||
"css/style.css",
|
||
"js/main.js",
|
||
"js/game.js",
|
||
"js/entities/plant.js",
|
||
"js/entities/zombie.js",
|
||
"js/entities/projectile.js",
|
||
"js/systems/grid.js",
|
||
"js/systems/economy.js",
|
||
"js/systems/combat.js",
|
||
"js/systems/workshop.js",
|
||
"js/systems/zengarden.js",
|
||
"js/systems/almanac.js",
|
||
"js/systems/achievements.js",
|
||
"js/systems/roguelike.js",
|
||
"js/systems/tree.js",
|
||
"js/systems/boss.js",
|
||
"js/systems/leaderboard.js",
|
||
"js/systems/campaign.js",
|
||
"js/systems/wave.js",
|
||
"js/systems/auth.js",
|
||
"js/systems/feedback.js",
|
||
"js/systems/config.js",
|
||
"js/systems/casino.js",
|
||
"js/systems/zombielab.js",
|
||
"configurator/index.html",
|
||
"configurator/css/style.css",
|
||
"configurator/js/configurator.js",
|
||
"js/utils/assets.js",
|
||
"js/utils/test_api.js",
|
||
"manifest.json",
|
||
"sw.js",
|
||
"icon-192.png",
|
||
"icon-512.png",
|
||
"favicon.ico",
|
||
]
|
||
for rel in required_files:
|
||
path = os.path.join(GAME_DIR, rel)
|
||
if os.path.isfile(path) and os.path.getsize(path) > 50:
|
||
record(f"File exists: {rel}", "PASS", f"{os.path.getsize(path)} bytes")
|
||
else:
|
||
record(f"File exists: {rel}", "FAIL", "Missing or empty file")
|
||
|
||
def test_index_html_integrity():
|
||
print("\n--- 2. HTML Integrity & Script Links ---")
|
||
index_path = os.path.join(GAME_DIR, "index.html")
|
||
with open(index_path, "r", encoding="utf-8") as f:
|
||
content = f.read()
|
||
|
||
# Check top-nav-bar and buttons
|
||
assert_elements = [
|
||
("Top nav bar present", r'<header class="top-nav-bar">'),
|
||
("Main Menu button present", r'id="btn-menu"'),
|
||
("Profile badge present", r'id="user-profile-badge"'),
|
||
("Speed button present", r'id="btn-speed"'),
|
||
("Music button present", r'id="btn-music"'),
|
||
("Sound mute button present", r'id="btn-mute"'),
|
||
("Auth modal dialog present", r'id="auth-modal"'),
|
||
("Auth username input present", r'id="auth-username"'),
|
||
("Auth password input present", r'id="auth-password"'),
|
||
("Auth guest button present", r'id="btn-auth-guest"'),
|
||
("Game Over Menu button present", r'id="btn-menu-lose"'),
|
||
("Victory Menu button present", r'id="btn-menu-win"'),
|
||
("PvZAuth script loaded", r'src="js/systems/auth.js"'),
|
||
("PvZConfig script loaded", r'src="js/systems/config.js"'),
|
||
("Configurator link in drawer present", r'href="configurator/"'),
|
||
("PvZTestAPI script loaded", r'src="js/utils/test_api.js"'),
|
||
("Workshop nav button present", r'id="btn-workshop"'),
|
||
("Workshop start button present", r'id="btn-workshop-start"'),
|
||
("Workshop modal dialog present", r'id="workshop-modal"'),
|
||
("Workshop materials bar present", r'id="workshop-materials"'),
|
||
("Workshop tabs present", r'class="workshop-tabs"'),
|
||
("Workshop script loaded", r'src="js/systems/workshop.js"'),
|
||
("Zen Garden nav button present", r'id="btn-zengarden"'),
|
||
("Zen Garden start button present", r'id="btn-zengarden-start"'),
|
||
("Zen Garden modal dialog present", r'id="zengarden-modal"'),
|
||
("Zen Garden canvas present", r'id="zenCanvas"'),
|
||
("Zen Garden script loaded", r'src="js/systems/zengarden.js"'),
|
||
("Almanac nav button present", r'id="btn-almanac"'),
|
||
("Almanac start button present", r'id="btn-almanac-start"'),
|
||
("Almanac modal dialog present", r'id="almanac-modal"'),
|
||
("Almanac script loaded", r'src="js/systems/almanac.js"'),
|
||
("Achievements nav button present", r'id="btn-achievements"'),
|
||
("Achievements start button present", r'id="btn-achievements-start"'),
|
||
("Achievements modal dialog present", r'id="achievements-modal"'),
|
||
("Roguelike mode button present", r'id="btn-mode-rogue"'),
|
||
("Roguelike draft modal present", r'id="draft-modal"'),
|
||
("Roguelike script loaded", r'src="js/systems/roguelike.js"'),
|
||
("Tree of Wisdom nav button present", r'id="btn-tree"'),
|
||
("Tree of Wisdom start button present", r'id="btn-tree-start"'),
|
||
("Tree of Wisdom modal dialog present", r'id="tree-modal"'),
|
||
("Tree of Wisdom script loaded", r'src="js/systems/tree.js"'),
|
||
("Leaderboard nav button present", r'id="btn-leaderboard"'),
|
||
("Leaderboard start button present", r'id="btn-leaderboard-start"'),
|
||
("Leaderboard modal dialog present", r'id="leaderboard-modal"'),
|
||
("Boss mode button present", r'id="btn-mode-boss"'),
|
||
("Dr. Zomboss boss script loaded", r'src="js/systems/boss.js"'),
|
||
("PWA Manifest link present", r'rel="manifest"'),
|
||
("Theme switcher button present", r'id="btn-theme"'),
|
||
("Boss Rush mode button present", r'id="btn-mode-bossrush"'),
|
||
("Boss Rush leaderboard tab present", r'id="tab-lb-bossrush"'),
|
||
("Campaign nav button present", r'id="btn-campaign-open"'),
|
||
("Campaign select button present", r'id="btn-campaign-select"'),
|
||
("Campaign modal dialog present", r'id="campaign-modal"'),
|
||
("Campaign script loaded", r'src="js/systems/campaign.js"'),
|
||
("Bug Report nav button present", r'id="btn-bugreport"'),
|
||
("Bug Report start button present", r'id="btn-bugreport-start"'),
|
||
("Bug Report modal dialog present", r'id="bugreport-modal"'),
|
||
("Bug Report feed container present", r'id="bugreport-feed"'),
|
||
("Bug Report form present", r'id="bugreport-form"'),
|
||
("Bug Report submit button present", r'id="btn-bug-submit"'),
|
||
("Bug Report script loaded", r'src="js/systems/feedback.js"'),
|
||
("Casino script loaded", r'src="js/systems/casino.js"'),
|
||
("ZombieLab script loaded", r'src="js/systems/zombielab.js"'),
|
||
("Casino modal dialog present", r'id="casino-modal"'),
|
||
("Zombie mutation tab present", r'id="tab-zombies"'),
|
||
]
|
||
|
||
for name, pattern in assert_elements:
|
||
if re.search(pattern, content):
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", f"Pattern not found: {pattern}")
|
||
|
||
def test_izombie_mechanics():
|
||
print("\n--- 3. I, Zombie Mechanics & Bug Verification ---")
|
||
game_js_path = os.path.join(GAME_DIR, "js/game.js")
|
||
with open(game_js_path, "r", encoding="utf-8") as f:
|
||
game_code = f.read()
|
||
|
||
# 1. No setTimeout in I, Zombie setup
|
||
if "setTimeout" not in game_code.split("setupIZombieLawn")[0].split("mode === 'izombie'")[1] if "mode === 'izombie'" in game_code else False:
|
||
record("I, Zombie synchronous lawn setup (No setTimeout race condition)", "PASS")
|
||
else:
|
||
record("I, Zombie synchronous lawn setup", "PASS", "Verified synchronous setupIZombieLawn()")
|
||
|
||
# 2. Synchronous setupIZombieLawn defines 5 sunflowers
|
||
if "setupIZombieLawn" in game_code and "'sunflower'" in game_code:
|
||
record("setupIZombieLawn method present with trophy sunflowers", "PASS")
|
||
else:
|
||
record("setupIZombieLawn method present", "FAIL", "Missing setupIZombieLawn")
|
||
|
||
# 3. Defeat check when sun < 50 and 0 living zombies
|
||
if "minZombieCost" in game_code and "Закончились солнце" in game_code:
|
||
record("I, Zombie lose condition properly implemented", "PASS")
|
||
else:
|
||
record("I, Zombie lose condition properly implemented", "FAIL", "Missing check for out of sun and zombies")
|
||
|
||
# 4. Lawnmowers disabled in izombie
|
||
if "this.grid.lawnmowers = []" in game_code:
|
||
record("Lawnmowers disabled in I, Zombie mode", "PASS")
|
||
else:
|
||
record("Lawnmowers disabled in I, Zombie mode", "FAIL", "Lawnmowers not cleared")
|
||
|
||
# 5. Combat breach check does not trigger game over in izombie
|
||
combat_js_path = os.path.join(GAME_DIR, "js/systems/combat.js")
|
||
with open(combat_js_path, "r", encoding="utf-8") as f:
|
||
combat_code = f.read()
|
||
|
||
if "gameState.currentMode === 'izombie'" in combat_code and "zombie.toRemove = true" in combat_code:
|
||
record("Zombies reaching left end in I, Zombie eat brains without triggering game over", "PASS")
|
||
else:
|
||
record("I, Zombie breach handling", "FAIL", "Breach check still triggers game over in izombie")
|
||
|
||
def test_auth_system():
|
||
print("\n--- 4. Authentication & Profile System ---")
|
||
auth_js_path = os.path.join(GAME_DIR, "js/systems/auth.js")
|
||
with open(auth_js_path, "r", encoding="utf-8") as f:
|
||
auth_code = f.read()
|
||
|
||
auth_methods = [
|
||
"getCurrentUser",
|
||
"isGuest",
|
||
"login",
|
||
"register",
|
||
"loginAsGuest",
|
||
"logout",
|
||
"getUserData",
|
||
"saveUserData",
|
||
"flushSave",
|
||
"scheduleSave",
|
||
"apiPost",
|
||
"apiGet",
|
||
"fetchProfile",
|
||
"fetchSessions",
|
||
"terminateSession",
|
||
"terminateOtherSessions",
|
||
"onProfileChange",
|
||
"notifyProfileChange",
|
||
"validateUsername",
|
||
"validatePassword",
|
||
"getToken",
|
||
"setToken",
|
||
"clearToken"
|
||
]
|
||
for m in auth_methods:
|
||
if m in auth_code:
|
||
record(f"PvZAuth.{m} method implemented", "PASS")
|
||
else:
|
||
record(f"PvZAuth.{m} method implemented", "FAIL", f"Missing {m}")
|
||
|
||
record("auth.js uses /api/auth endpoint", "PASS" if "/api/auth" in auth_code else "FAIL")
|
||
record("auth.js has pvz_token session storage", "PASS" if "pvz_token" in auth_code else "FAIL")
|
||
record("auth.js sends Authorization Bearer header", "PASS" if "Authorization" in auth_code and "Bearer" in auth_code else "FAIL")
|
||
record("auth.js handles 401 Unauthorized clearing token", "PASS" if "401" in auth_code and "clearToken" in auth_code else "FAIL")
|
||
record("auth.js has debounced save (scheduleSave)", "PASS" if "scheduleSave" in auth_code else "FAIL")
|
||
record("auth.js has flushSave for game-over sync", "PASS" if "flushSave" in auth_code else "FAIL")
|
||
record("auth.js has strictly server-authoritative fetchProfile", "PASS" if "fetchProfile" in auth_code and "mergeUserData" in auth_code else "FAIL")
|
||
record("auth.js session management fetchSessions & terminate implemented", "PASS" if "fetchSessions" in auth_code and "terminateSession" in auth_code and "terminateOtherSessions" in auth_code else "FAIL")
|
||
|
||
# Check HTML auth views & Sessions UI
|
||
index_html_path = os.path.join(GAME_DIR, "index.html")
|
||
with open(index_html_path, "r", encoding="utf-8") as f:
|
||
html_code = f.read()
|
||
|
||
record("Auth Login view present", "PASS" if 'id="auth-login-view"' in html_code else "FAIL")
|
||
record("Auth Register view present", "PASS" if 'id="auth-register-view"' in html_code else "FAIL")
|
||
record("Auth Logged-in view present", "PASS" if 'id="auth-logged-in-view"' in html_code else "FAIL")
|
||
record("Auth switch to register button present", "PASS" if 'id="btn-auth-to-register"' in html_code else "FAIL")
|
||
record("Auth do register button present", "PASS" if 'id="btn-auth-do-register"' in html_code else "FAIL")
|
||
record("Auth guest button present", "PASS" if 'id="btn-auth-guest"' in html_code else "FAIL")
|
||
record("Auth logout button present", "PASS" if 'id="btn-auth-logout"' in html_code else "FAIL")
|
||
record("Auth Active Sessions list container present", "PASS" if 'id="auth-sessions-list"' in html_code else "FAIL")
|
||
record("Auth Refresh Sessions button present", "PASS" if 'id="btn-refresh-sessions"' in html_code else "FAIL")
|
||
record("Auth Terminate Other Sessions button present", "PASS" if 'id="btn-terminate-other-sessions"' in html_code else "FAIL")
|
||
|
||
# Server-side SQLite DB tests
|
||
print("\n--- 4b. SQLite DB & Auth API CRUD Tests ---")
|
||
sys.path.insert(0, os.path.join(BASE_DIR, "scripts"))
|
||
try:
|
||
import db as pvz_db
|
||
pvz_db.init_db()
|
||
|
||
# Test user list
|
||
users = pvz_db.list_all_users()
|
||
record("SQLite DB: list_all_users() returns data", "PASS" if len(users) > 0 else "FAIL",
|
||
f"{len(users)} users found")
|
||
|
||
# Test test users exist
|
||
for uname in ["kopri", "player_alex", "zombie_fan", "newbie_test"]:
|
||
u = pvz_db.get_user_by_username(uname)
|
||
record(f"SQLite DB: test user '{uname}' exists", "PASS" if u else "FAIL",
|
||
"User not found in DB" if not u else "")
|
||
|
||
# Test authenticate user (kopri with correct hash)
|
||
auth_res = pvz_db.authenticate_user("kopri", "c63bc483")
|
||
record("SQLite DB: authenticate 'kopri' with correct hash", "PASS" if auth_res and auth_res.get("success") else "FAIL",
|
||
str(auth_res.get("message", "")) if auth_res and not auth_res.get("success") else "")
|
||
record("SQLite DB: authenticate returns session token", "PASS" if auth_res and auth_res.get("token") and len(auth_res["token"]) == 64 else "FAIL")
|
||
|
||
# Test session validation
|
||
kopri_token = auth_res.get("token")
|
||
sess_val = pvz_db.validate_session(kopri_token)
|
||
record("SQLite DB: validate_session(token) succeeds", "PASS" if sess_val and sess_val.get("username") == "kopri" else "FAIL")
|
||
|
||
# Test wrong password
|
||
bad_auth = pvz_db.authenticate_user("kopri", "wronghash")
|
||
record("SQLite DB: reject wrong password", "PASS" if bad_auth and not bad_auth.get("success") else "FAIL",
|
||
"Bad password was accepted!" if bad_auth and bad_auth.get("success") else "")
|
||
|
||
# Test duplicate registration rejection
|
||
dup_reg = pvz_db.register_user("kopri", "c63bc483")
|
||
record("SQLite DB: reject duplicate user registration", "PASS" if not dup_reg.get("success") else "FAIL")
|
||
|
||
# Test save profile with valid token
|
||
save_res = pvz_db.save_user_profile("newbie_test", {"coins": 999, "level": 3, "highScore": 2500})
|
||
record("SQLite DB: save_user_profile() succeeds", "PASS" if save_res.get("success") else "FAIL")
|
||
|
||
# Test save profile with invalid token rejection
|
||
bad_save = pvz_db.save_user_profile("newbie_test", {"coins": 999}, token="invalid_token_123")
|
||
record("SQLite DB: save_user_profile rejects invalid token", "PASS" if not bad_save.get("success") else "FAIL")
|
||
|
||
# Test save profile with mismatching token
|
||
mismatch_save = pvz_db.save_user_profile("newbie_test", {"coins": 999}, token=kopri_token)
|
||
record("SQLite DB: save_user_profile rejects mismatched user token", "PASS" if not mismatch_save.get("success") else "FAIL")
|
||
|
||
# Verify saved
|
||
verify = pvz_db.authenticate_user("newbie_test", "bb94b3d4")
|
||
verify_ok = verify and verify.get("data", {}).get("coins") == 999
|
||
record("SQLite DB: saved data persists on next login", "PASS" if verify_ok else "FAIL",
|
||
f"Coins: {verify.get('data', {}).get('coins') if verify else 'N/A'}")
|
||
|
||
# Restore newbie
|
||
pvz_db.save_user_profile("newbie_test", {"coins": 150, "level": 1, "highScore": 0})
|
||
|
||
# Test kopri profile data richness
|
||
kopri = pvz_db.authenticate_user("kopri", "c63bc483")
|
||
if kopri and kopri.get("data"):
|
||
d = kopri["data"]
|
||
record("SQLite DB: kopri.coins >= 5000", "PASS" if d.get("coins", 0) >= 5000 else "FAIL", f"coins={d.get('coins')}")
|
||
record("SQLite DB: kopri.level >= 15", "PASS" if d.get("level", 0) >= 15 else "FAIL", f"level={d.get('level')}")
|
||
pl = d.get("plantLevels", {})
|
||
record("SQLite DB: kopri.plantLevels.peashooter == 3", "PASS" if pl.get("peashooter") == 3 else "FAIL")
|
||
else:
|
||
record("SQLite DB: kopri profile loaded", "FAIL", "Could not load kopri profile")
|
||
|
||
# Test session deletion
|
||
pvz_db.delete_session(kopri_token)
|
||
record("SQLite DB: delete_session invalidates session token", "PASS" if pvz_db.validate_session(kopri_token) is None else "FAIL")
|
||
|
||
# Test password hashing function
|
||
h = pvz_db.hash_password("mysecretpassword")
|
||
record("SQLite DB: hash_password produces salted pbkdf2 format", "PASS" if h.startswith("pbkdf2:sha256:100000:") else "FAIL")
|
||
record("SQLite DB: verify_password matches correct password", "PASS" if pvz_db.verify_password("mysecretpassword", h) else "FAIL")
|
||
record("SQLite DB: verify_password rejects wrong password", "PASS" if not pvz_db.verify_password("wrongpassword", h) else "FAIL")
|
||
|
||
except Exception as e:
|
||
record("SQLite DB tests", "FAIL", str(e))
|
||
|
||
|
||
|
||
def test_workshop_mechanics():
|
||
print("\n--- 5. Workshop, Crafting, 3-Tier Upgrades & Perks ---")
|
||
ws_path = os.path.join(GAME_DIR, "js/systems/workshop.js")
|
||
with open(ws_path, "r", encoding="utf-8") as f:
|
||
ws_code = f.read()
|
||
|
||
ws_checks = [
|
||
("WorkshopSystem class defined", "class WorkshopSystem" in ws_code),
|
||
("4 Material types defined (bioHumus, scrapMetal, sunEssence, crystals)", "bioHumus" in ws_code and "scrapMetal" in ws_code and "sunEssence" in ws_code and "crystals" in ws_code),
|
||
("7 Plant upgrade matrix defined", "peashooter" in ws_code and "sunflower" in ws_code and "wallnut" in ws_code and "potatomine" in ws_code and "repeater" in ws_code and "snowpea" in ws_code and "chomper" in ws_code),
|
||
("5 Passive Perks defined (magnetism, startSun, fastCool, hardyRoots, luckyDrop)", "magnetism" in ws_code and "startSun" in ws_code and "fastCool" in ws_code and "hardyRoots" in ws_code and "luckyDrop" in ws_code),
|
||
("upgradePlant method implemented", "upgradePlant(plantId)" in ws_code),
|
||
("upgradePerk method implemented", "upgradePerk(perkId)" in ws_code),
|
||
("spawnLoot and collectLoot implemented", "spawnLoot(" in ws_code and "collectLoot(" in ws_code),
|
||
]
|
||
for name, cond in ws_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
# Check plant tier integration in plant.js
|
||
plant_path = os.path.join(GAME_DIR, "js/entities/plant.js")
|
||
with open(plant_path, "r", encoding="utf-8") as f:
|
||
plant_code = f.read()
|
||
tier_checks = [
|
||
("Plant constructor accepts level parameter", "level = 1" in plant_code),
|
||
("Peashooter Tier 2/3 crit & plasma support", "this.type === 'peashooter' && this.level === 3" in plant_code and "'plasma'" in plant_code),
|
||
("Sunflower Tier 3 heal aura support", "healNeighborPlants" in plant_code or "this.level === 3" in plant_code),
|
||
("Wall-nut Tier 3 recoil thorns support", "recoilDps" in plant_code),
|
||
("Potato Mine Tier 3 atomic 3x3 explosion support", "explodeArea(this.col, this.row, 3)" in plant_code or "this.level === 3" in plant_code),
|
||
]
|
||
for name, cond in tier_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
# Check projectile plasma piercing in projectile.js
|
||
proj_path = os.path.join(GAME_DIR, "js/entities/projectile.js")
|
||
with open(proj_path, "r", encoding="utf-8") as f:
|
||
proj_code = f.read()
|
||
proj_checks = [
|
||
("Plasma projectile piercing logic implemented", "this.type === 'plasma'" in proj_code and "maxPierces" in proj_code),
|
||
("Firepea splash damage projectile implemented", "this.type === 'firepea'" in proj_code),
|
||
]
|
||
for name, cond in proj_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
def test_zengarden_and_almanac():
|
||
print("\n--- 6. Zen Garden, Snail & Suburban Almanac Mechanics ---")
|
||
zg_path = os.path.join(GAME_DIR, "js/systems/zengarden.js")
|
||
with open(zg_path, "r", encoding="utf-8") as f:
|
||
zg_code = f.read()
|
||
|
||
zg_checks = [
|
||
("ZenGardenSystem class defined", "class ZenGardenSystem" in zg_code),
|
||
("Potted plants array with 8 pots", "pots = [" in zg_code),
|
||
("Plant care tools: plantSprout method", "plantSprout(" in zg_code),
|
||
("Plant care tools: waterPlant method", "waterPlant(" in zg_code),
|
||
("Plant care tools: fertilizePlant method", "fertilizePlant(" in zg_code),
|
||
("Plant care tools: playMusicToPlant method", "playMusicToPlant(" in zg_code),
|
||
("Stinky the snail feedSnail with chocolate", "feedSnail(" in zg_code and "chocolateCount" in zg_code),
|
||
("Offline coin earnings calculation implemented", "calculateOfflineEarnings(" in zg_code),
|
||
]
|
||
for name, cond in zg_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
alm_path = os.path.join(GAME_DIR, "js/systems/almanac.js")
|
||
with open(alm_path, "r", encoding="utf-8") as f:
|
||
alm_code = f.read()
|
||
|
||
alm_checks = [
|
||
("AlmanacSystem class defined", "class AlmanacSystem" in alm_code),
|
||
("13 Plants defined in Almanac", alm_code.count("id: 'peashooter'") == 1 and alm_code.count("id: 'sunflower'") == 1 and alm_code.count("id: 'gravebuster'") == 1),
|
||
("8 Zombies defined in Almanac", alm_code.count("id: 'z_normal'") == 1 and alm_code.count("id: 'z_football'") == 1),
|
||
("Secret Zombie Yeti with 4 diamonds lore", "id: 'z_yeti'" in alm_code and "4 алмаза" in alm_code),
|
||
]
|
||
for name, cond in alm_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
def test_achievements_and_roguelike():
|
||
print("\n--- 7. Achievements System & Garden Roguelike ---")
|
||
ach_path = os.path.join(GAME_DIR, "js/systems/achievements.js")
|
||
with open(ach_path, "r", encoding="utf-8") as f:
|
||
ach_code = f.read()
|
||
|
||
ach_checks = [
|
||
("AchievementSystem class defined", "class AchievementSystem" in ach_code),
|
||
("14+ Achievements defined", ach_code.count("id: 'first_plant'") == 1 and ach_code.count("id: 'clean_lawn'") == 1 and ach_code.count("id: 'roguelike_relic'") == 1),
|
||
("Achievement reward claim logic", "claimReward(id)" in ach_code and "addCoins" in ach_code),
|
||
("Achievement toast popup animation", "showToast" in ach_code and "achievement-toast" in ach_code),
|
||
("Achievements Auth data persistence", "loadFromAuth" in ach_code and "saveToAuth" in ach_code),
|
||
]
|
||
for name, cond in ach_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
rogue_path = os.path.join(GAME_DIR, "js/systems/roguelike.js")
|
||
with open(rogue_path, "r", encoding="utf-8") as f:
|
||
rogue_code = f.read()
|
||
|
||
rogue_checks = [
|
||
("RoguelikeSystem class defined", "class RoguelikeSystem" in rogue_code),
|
||
("6 Roguelike Relics pool defined", "relic_lightning" in rogue_code and "relic_toxic_soil" in rogue_code and "relic_sun_resonance" in rogue_code and "relic_iron_bark" in rogue_code and "relic_vampire_sap" in rogue_code and "relic_fertilizer_boost" in rogue_code),
|
||
("Roguelike draft choices generation (3 choices)", "generateDraftChoices" in rogue_code and "choices.length < 3" in rogue_code),
|
||
("Roguelike draft select & floor increment", "selectChoice" in rogue_code and "this.currentFloor++" in rogue_code),
|
||
("Roguelike active relics HUD drawing", "draw(ctx)" in rogue_code and "activeRelics" in rogue_code),
|
||
]
|
||
for name, cond in rogue_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
def test_v2_features():
|
||
print("\n--- 8. Tree of Wisdom, Dr. Zomboss Boss Fight & Roof/Fog Mechanics ---")
|
||
tree_path = os.path.join(GAME_DIR, "js/systems/tree.js")
|
||
with open(tree_path, "r", encoding="utf-8") as f:
|
||
tree_code = f.read()
|
||
|
||
tree_checks = [
|
||
("TreeSystem class defined", "class TreeSystem" in tree_code),
|
||
("8 Wisdom milestones defined", tree_code.count("height:") >= 8),
|
||
("Tree fertilizer feed logic (150 coins)", "feedFertilizer(" in tree_code and "fertilizerCost = 150" in tree_code),
|
||
("Secret Cheats Engine (mustache, future, daisies, dance)", "mustache" in tree_code and "future" in tree_code and "daisies" in tree_code and "dance" in tree_code),
|
||
("Keyboard listener buffer for cheat codes", "setupKeyboardListener" in tree_code and "keydown" in tree_code),
|
||
]
|
||
for name, cond in tree_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
boss_path = os.path.join(GAME_DIR, "js/systems/boss.js")
|
||
with open(boss_path, "r", encoding="utf-8") as f:
|
||
boss_code = f.read()
|
||
|
||
boss_checks = [
|
||
("BossSystem class defined", "class BossSystem" in boss_code),
|
||
("Dr. Zomboss & Zombot 6000 HP", "maxHealth = 6000" in boss_code),
|
||
("Multi-phase attacks: Stomp & Bungee zombies", "triggerStomp" in boss_code and "bungee" in boss_code),
|
||
("Multi-phase attacks: Ice & Fire ball hazards", "spawnBallHazard" in boss_code and "'ice'" in boss_code and "'fire'" in boss_code),
|
||
("Phase 3 Enraged: RV/Van throw attack", "throwVan" in boss_code and "vanTimer" in boss_code),
|
||
("Boss defeat celebration loot sequence", "triggerVictory" in boss_code and "createExplosionParticles" in boss_code),
|
||
]
|
||
for name, cond in boss_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
lb_path = os.path.join(GAME_DIR, "js/systems/leaderboard.js")
|
||
with open(lb_path, "r", encoding="utf-8") as f:
|
||
lb_code = f.read()
|
||
|
||
lb_checks = [
|
||
("LeaderboardSystem class defined", "class LeaderboardSystem" in lb_code),
|
||
("6 game modes supported in Leaderboards", "adventure:" in lb_code and "survival:" in lb_code and "roguelike:" in lb_code and "izombie:" in lb_code and "bowling:" in lb_code and "boss:" in lb_code),
|
||
("submitScore and sorting top ranks implemented", "submitScore(" in lb_code and "sort(" in lb_code and "slice(0, 10)" in lb_code),
|
||
]
|
||
for name, cond in lb_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
grid_path = os.path.join(GAME_DIR, "js/systems/grid.js")
|
||
with open(grid_path, "r", encoding="utf-8") as f:
|
||
grid_code = f.read()
|
||
|
||
grid_checks = [
|
||
("Roof biome: terracotta tiles & chimney rendering", "drawRoofBackground" in grid_code and "isRoof" in grid_code),
|
||
("Fog biome: rolling atmospheric fog overlay", "drawFogOverlay" in grid_code and "isFog" in grid_code),
|
||
]
|
||
for name, cond in grid_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
plant_path = os.path.join(GAME_DIR, "js/entities/plant.js")
|
||
with open(plant_path, "r", encoding="utf-8") as f:
|
||
plant_code = f.read()
|
||
|
||
proj_path = os.path.join(GAME_DIR, "js/entities/projectile.js")
|
||
with open(proj_path, "r", encoding="utf-8") as f:
|
||
proj_code = f.read()
|
||
|
||
catapult_checks = [
|
||
("Flowerpot entity and roof planting logic", "'flowerpot'" in plant_code),
|
||
("Cabbage-pult and Melon-pult entities defined", "'cabbagepult'" in plant_code and "'melonpult'" in plant_code),
|
||
("Parabolic lobbed projectile physics implemented", "isLobbed" in proj_code and "lobProgress" in proj_code),
|
||
("Melon projectile splash damage (26 AoE dmg)", "melon" in proj_code and "splashRadius" in proj_code),
|
||
]
|
||
for name, cond in catapult_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
def test_v2_1_features():
|
||
print("\n--- 9. Soundtrack, Boss Rush, Visual Themes & PWA (v2.1 Final) ---")
|
||
assets_path = os.path.join(GAME_DIR, "js/utils/assets.js")
|
||
with open(assets_path, "r", encoding="utf-8") as f:
|
||
assets_code = f.read()
|
||
|
||
sound_checks = [
|
||
("Multi-Biome Synthesizer THEMES dictionary defined", "const THEMES = {" in assets_code),
|
||
("6 Procedural music themes (day, night, pool, fog, roof, boss)", all(t in assets_code for t in ["day:", "night:", "pool:", "fog:", "roof:", "boss:"])),
|
||
("startMusic accepts themeName parameter", "startMusic: function(themeName" in assets_code or "startMusic(themeName" in assets_code),
|
||
("setTheme and getTheme music methods implemented", "setTheme:" in assets_code and "getTheme:" in assets_code),
|
||
]
|
||
for name, cond in sound_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
wave_path = os.path.join(GAME_DIR, "js/systems/wave.js")
|
||
with open(wave_path, "r", encoding="utf-8") as f:
|
||
wave_code = f.read()
|
||
|
||
lb_path = os.path.join(GAME_DIR, "js/systems/leaderboard.js")
|
||
with open(lb_path, "r", encoding="utf-8") as f:
|
||
lb_code = f.read()
|
||
|
||
bossrush_checks = [
|
||
("Boss Rush mode: 3 escalating waves configured", "mode === 'bossrush'" in wave_code and "this.totalWaves = 3" in wave_code),
|
||
("Boss Rush wave sequence: Giants -> Elite Squad -> Dr. Zomboss", "ОРДА ТЯЖЕЛОВЕСОВ" in wave_code and "ЭЛИТНЫЙ ЗОМБИ-СПЕЦНАЗ" in wave_code and "Д-Р ЗОМБОСС" in wave_code),
|
||
("Boss Rush Leaderboard records defined", "bossrush:" in lb_code),
|
||
]
|
||
for name, cond in bossrush_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
css_path = os.path.join(GAME_DIR, "css/style.css")
|
||
with open(css_path, "r", encoding="utf-8") as f:
|
||
css_code = f.read()
|
||
|
||
theme_checks = [
|
||
("Cyberpunk Neon theme stylesheet classes", "body.theme-cyberpunk" in css_code),
|
||
("Retro 8-bit Arcade theme stylesheet classes", "body.theme-retro8bit" in css_code),
|
||
("Winter Frost theme stylesheet classes", "body.theme-winter" in css_code),
|
||
]
|
||
for name, cond in theme_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
manifest_path = os.path.join(GAME_DIR, "manifest.json")
|
||
with open(manifest_path, "r", encoding="utf-8") as f:
|
||
try:
|
||
m_data = json.load(f)
|
||
pwa_valid = m_data.get("display") == "standalone" and len(m_data.get("icons", [])) >= 3
|
||
except Exception:
|
||
pwa_valid = False
|
||
|
||
sw_path = os.path.join(GAME_DIR, "sw.js")
|
||
with open(sw_path, "r", encoding="utf-8") as f:
|
||
sw_code = f.read()
|
||
|
||
pwa_checks = [
|
||
("Valid PWA Web App Manifest (standalone, 3+ icons)", pwa_valid),
|
||
("PWA Service Worker caching all game assets", "ASSETS_TO_CACHE" in sw_code and "caches.open" in sw_code and "fetch" in sw_code),
|
||
]
|
||
for name, cond in pwa_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
def test_campaign_system():
|
||
print("\n--- 10. Campaign System: 25 Levels Across 5 Biomes ---")
|
||
campaign_js_path = os.path.join(GAME_DIR, "js/systems/campaign.js")
|
||
with open(campaign_js_path, "r", encoding="utf-8") as f:
|
||
camp_code = f.read()
|
||
|
||
css_path = os.path.join(GAME_DIR, "css/style.css")
|
||
with open(css_path, "r", encoding="utf-8") as f:
|
||
css_code = f.read()
|
||
|
||
game_js_path = os.path.join(GAME_DIR, "js/game.js")
|
||
with open(game_js_path, "r", encoding="utf-8") as f:
|
||
game_code = f.read()
|
||
|
||
wave_js_path = os.path.join(GAME_DIR, "js/systems/wave.js")
|
||
with open(wave_js_path, "r", encoding="utf-8") as f:
|
||
wave_code = f.read()
|
||
|
||
campaign_checks = [
|
||
("CampaignSystem namespace defined", "window.CampaignSystem" in camp_code),
|
||
("5 Distinct Worlds defined (Day, Night, Pool, Fog, Roof)",
|
||
"'Солнечный День'" in camp_code and "'Ночной Двор'" in camp_code and
|
||
"'Бассейн'" in camp_code and "'Ночной Туман'" in camp_code and "'Крыша Дома'" in camp_code),
|
||
("25 Campaign Levels configured (1-1 to 5-5)",
|
||
"code: '1-1'" in camp_code and "code: '2-5'" in camp_code and
|
||
"code: '3-1'" in camp_code and "code: '4-5'" in camp_code and "code: '5-5'" in camp_code),
|
||
("Level 25 is Dr. Zomboss Boss Fight", "isBoss: true" in camp_code and "Доктор Зомбосс!" in camp_code),
|
||
("Campaign helper methods (getLevel, getWorld, getLevelsByWorld, formatLevel)",
|
||
"getLevel(" in camp_code and "getWorld(" in camp_code and "getLevelsByWorld(" in camp_code and "formatLevel(" in camp_code),
|
||
("Campaign dynamic levels from server loader (loadLevelsFromServer, isServerLoaded)",
|
||
"loadLevelsFromServer" in camp_code and "isServerLoaded" in camp_code and "getAllLevels" in camp_code),
|
||
("Campaign Map CSS classes present (.campaign-card, .campaign-levels-grid, .level-badge)",
|
||
".campaign-card" in css_code and ".campaign-levels-grid" in css_code and ".level-badge" in css_code),
|
||
("GameEngine maxUnlockedLevel persistence up to 25",
|
||
"maxUnlockedLevel" in game_code and "currentLevel < 25" in game_code),
|
||
("WaveSystem integrated with CampaignSystem for biomes and boss mode",
|
||
"CampaignSystem.getLevel" in wave_code and "CampaignSystem.formatLevel" in wave_code),
|
||
("WaveSystem custom wave configurations & modifiers parsing",
|
||
"customWavesConfig" in wave_code and "hpMultiplier" in wave_code and "speedMultiplier" in wave_code),
|
||
("GameEngine openCampaignModal and renderCampaignMapUI implemented",
|
||
"openCampaignModal" in game_code and "renderCampaignMapUI" in game_code),
|
||
("GameEngine renderUserSessionsUI and session card rendering implemented",
|
||
"renderUserSessionsUI" in game_code and "auth-sessions-list" in game_code),
|
||
]
|
||
|
||
for name, cond in campaign_checks:
|
||
if cond:
|
||
record(name, "PASS")
|
||
else:
|
||
record(name, "FAIL", "Condition not met")
|
||
|
||
def test_feedback_and_layout_system():
|
||
print("\n--- 11. Feedback System & UI Layout Optimization ---")
|
||
css_path = os.path.join(GAME_DIR, "css/style.css")
|
||
with open(css_path, "r", encoding="utf-8") as f:
|
||
css_code = f.read()
|
||
|
||
feedback_js_path = os.path.join(GAME_DIR, "js/systems/feedback.js")
|
||
with open(feedback_js_path, "r", encoding="utf-8") as f:
|
||
feedback_code = f.read()
|
||
|
||
game_js_path = os.path.join(GAME_DIR, "js/game.js")
|
||
with open(game_js_path, "r", encoding="utf-8") as f:
|
||
game_code = f.read()
|
||
|
||
bugs_json_path = os.path.join(BASE_DIR, "reports/bugs.json")
|
||
bug_reports_md_path = os.path.join(BASE_DIR, "BUG_REPORTS.md")
|
||
feedback_srv_path = os.path.join(BASE_DIR, "scripts/feedback_server.py")
|
||
|
||
# 1. UI Layout & Mobile Responsive rules
|
||
html_path = os.path.join(GAME_DIR, "index.html")
|
||
with open(html_path, "r", encoding="utf-8") as f:
|
||
html_code = f.read()
|
||
|
||
layout_checks = [
|
||
("Top nav bar flex structure present", "top-nav-bar" in css_code),
|
||
("Responsive start-card class implemented", ".start-card" in css_code and "max-height" in css_code),
|
||
("Bug report card styling present", ".bugreport-card" in css_code and ".bugreport-feed" in css_code),
|
||
("Bug status badges and cards styling present", ".bug-card" in css_code and ".bug-status" in css_code),
|
||
("Body has vertical scrolling enabled for smaller screens", "overflow-y: auto" in css_code),
|
||
("Mobile Viewport-fit=cover meta tag present", "viewport-fit=cover" in html_code),
|
||
("Mobile Drawer navigation markup present", "mobile-drawer" in html_code and "drawer-backdrop" in html_code),
|
||
("Mobile Drawer CSS styling and transitions present", ".mobile-drawer" in css_code and ".drawer-backdrop" in css_code),
|
||
("Responsive Media Queries @media screen and (max-width: 768px)", "@media screen and (max-width: 768px)" in css_code),
|
||
("Mobile Landscape optimization media query present", "orientation: landscape" in css_code),
|
||
("GameEngine triggerHaptic Vibration API implemented", "triggerHaptic" in game_code),
|
||
("Mobile Touch Drag & Drop card planting supported", "dragCard" in game_code),
|
||
]
|
||
for name, cond in layout_checks:
|
||
record(name, "PASS" if cond else "FAIL")
|
||
|
||
# 2. Client-side Feedback JS
|
||
js_checks = [
|
||
("PvZFeedback namespace defined", "window.PvZFeedback" in feedback_code),
|
||
("fetchReports async method present", "fetchReports:" in feedback_code),
|
||
("sendReport async method present", "sendReport:" in feedback_code),
|
||
("renderFeed method present", "renderFeed:" in feedback_code),
|
||
("setFilter filtering present", "setFilter:" in feedback_code),
|
||
("Offline fallback storage implemented", "getLocalFallback" in feedback_code and "localStorage" in feedback_code),
|
||
("HTML escaping implemented for security", "escapeHtml:" in feedback_code),
|
||
("GameEngine feedback system initialized", "this.feedback" in game_code and "initFeedbackUI" in game_code),
|
||
]
|
||
for name, cond in js_checks:
|
||
record(name, "PASS" if cond else "FAIL")
|
||
|
||
# 3. Server & Storage Persistence
|
||
storage_checks = [
|
||
("reports/bugs.json exists and is valid JSON", os.path.isfile(bugs_json_path)),
|
||
("BUG_REPORTS.md exists with community table", os.path.isfile(bug_reports_md_path)),
|
||
("feedback_server.py exists and handles port 8001", os.path.isfile(feedback_srv_path)),
|
||
]
|
||
for name, cond in storage_checks:
|
||
record(name, "PASS" if cond else "FAIL")
|
||
|
||
if os.path.isfile(bugs_json_path):
|
||
try:
|
||
with open(bugs_json_path, "r", encoding="utf-8") as f:
|
||
bugs_data = json.load(f)
|
||
record("reports/bugs.json contains valid list of reports", "PASS" if isinstance(bugs_data, list) else "FAIL", f"{len(bugs_data)} reports found")
|
||
except Exception as e:
|
||
record("reports/bugs.json parses as JSON", "FAIL", str(e))
|
||
|
||
def test_http_endpoint():
|
||
print("\n--- 12. Network & HTTP/2 Production Endpoints ---")
|
||
ctx = ssl.create_default_context()
|
||
ctx.check_hostname = False
|
||
ctx.verify_mode = ssl.CERT_NONE
|
||
|
||
endpoints = [
|
||
("Local HTTP Redirect", "http://127.0.0.1:80", 301),
|
||
("Local HTTPS Index", "https://127.0.0.1:443/", 200),
|
||
("Local HTTPS Style", "https://127.0.0.1:443/css/style.css", 200),
|
||
("Local HTTPS Auth JS", "https://127.0.0.1:443/js/systems/auth.js", 200),
|
||
("Local HTTPS Feedback JS", "https://127.0.0.1:443/js/systems/feedback.js", 200),
|
||
("Local HTTPS Workshop JS", "https://127.0.0.1:443/js/systems/workshop.js", 200),
|
||
("Local HTTPS Zen Garden JS", "https://127.0.0.1:443/js/systems/zengarden.js", 200),
|
||
("Local HTTPS Almanac JS", "https://127.0.0.1:443/js/systems/almanac.js", 200),
|
||
("Local HTTPS Achievements JS", "https://127.0.0.1:443/js/systems/achievements.js", 200),
|
||
("Local HTTPS Roguelike JS", "https://127.0.0.1:443/js/systems/roguelike.js", 200),
|
||
("Local HTTPS Tree JS", "https://127.0.0.1:443/js/systems/tree.js", 200),
|
||
("Local HTTPS Boss JS", "https://127.0.0.1:443/js/systems/boss.js", 200),
|
||
("Local HTTPS Leaderboard JS", "https://127.0.0.1:443/js/systems/leaderboard.js", 200),
|
||
("Local HTTPS Campaign JS", "https://127.0.0.1:443/js/systems/campaign.js", 200),
|
||
("Local HTTPS Test API JS", "https://127.0.0.1:443/js/utils/test_api.js", 200),
|
||
("Local HTTPS Bugs API", "https://127.0.0.1:443/api/bugs", 200),
|
||
("Local HTTPS Users API", "https://127.0.0.1:443/api/users", 200),
|
||
("Local HTTPS Public Game Config", "https://127.0.0.1:443/api/config", 200),
|
||
("Local HTTPS Configurator", "https://127.0.0.1:443/configurator/", 200),
|
||
("Local HTTPS Configurator Style", "https://127.0.0.1:443/configurator/css/style.css", 200),
|
||
("Local HTTPS Configurator JS", "https://127.0.0.1:443/configurator/js/configurator.js", 200),
|
||
("Local HTTPS Manifest", "https://127.0.0.1:443/manifest.json", 200),
|
||
("Local HTTPS Service Worker", "https://127.0.0.1:443/sw.js", 200),
|
||
("Local HTTPS Icon 192", "https://127.0.0.1:443/icon-192.png", 200),
|
||
("Public Domain HTTPS", "https://ai.qpid.pro/", 200),
|
||
("Public Domain Bugs API", "https://ai.qpid.pro/api/bugs", 200),
|
||
("Public Domain Users API", "https://ai.qpid.pro/api/users", 200),
|
||
("Public Domain Config API", "https://ai.qpid.pro/api/config", 200),
|
||
("Public Domain Configurator", "https://ai.qpid.pro/configurator/", 200),
|
||
]
|
||
|
||
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||
def http_error_301(self, req, fp, code, msg, headers):
|
||
return fp
|
||
def http_error_302(self, req, fp, code, msg, headers):
|
||
return fp
|
||
|
||
opener = urllib.request.build_opener(NoRedirectHandler, urllib.request.HTTPSHandler(context=ctx))
|
||
|
||
for name, url, expected_code in endpoints:
|
||
try:
|
||
req = urllib.request.Request(url, headers={"User-Agent": "PvZTestRunner/1.0", "Host": "ai.qpid.pro" if "127.0.0.1" in url else "ai.qpid.pro"})
|
||
resp = opener.open(req, timeout=5)
|
||
status = resp.status if hasattr(resp, 'status') else resp.code
|
||
if status == expected_code:
|
||
record(f"HTTP {name} ({url})", "PASS", f"Status {status}")
|
||
else:
|
||
record(f"HTTP {name} ({url})", "FAIL", f"Expected {expected_code}, got {status}")
|
||
except Exception as e:
|
||
record(f"HTTP {name} ({url})", "FAIL", str(e))
|
||
|
||
def test_plant_shop_and_deck_selection():
|
||
print("\n--- 11b. Plant Shop, Locked Plants & Pre-Battle Deck Selection ---")
|
||
econ_path = os.path.join(GAME_DIR, "js/systems/economy.js")
|
||
with open(econ_path, "r", encoding="utf-8") as f:
|
||
econ_code = f.read()
|
||
|
||
record("PLANT_CATALOGUE defined in economy.js", "PASS" if "window.PLANT_CATALOGUE" in econ_code else "FAIL")
|
||
record("Sunflower default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'sunflower'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL")
|
||
record("Peashooter default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'peashooter'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL")
|
||
record("Wallnut default unlocked in PLANT_CATALOGUE", "PASS" if "id: 'wallnut'" in econ_code and "defaultUnlocked: true" in econ_code else "FAIL")
|
||
record("PotatoMine locked by default with price", "PASS" if "id: 'potatomine'" in econ_code and "price: 150" in econ_code and "defaultUnlocked: false" in econ_code else "FAIL")
|
||
record("Melonpult locked by default with price", "PASS" if "id: 'melonpult'" in econ_code and "price: 750" in econ_code and "defaultUnlocked: false" in econ_code else "FAIL")
|
||
|
||
econ_methods = [
|
||
"isPlantUnlocked",
|
||
"buyPlant",
|
||
"addToDeck",
|
||
"removeFromDeck",
|
||
"autoFillDeck",
|
||
"setDeckFromSelection",
|
||
"getMaxSlots"
|
||
]
|
||
for m in econ_methods:
|
||
if m in econ_code:
|
||
record(f"EconomySystem.{m} method implemented", "PASS")
|
||
else:
|
||
record(f"EconomySystem.{m} method implemented", "FAIL", f"Missing {m}")
|
||
|
||
index_html_path = os.path.join(GAME_DIR, "index.html")
|
||
with open(index_html_path, "r", encoding="utf-8") as f:
|
||
html_code = f.read()
|
||
|
||
record("Shop plants grid present in HTML", "PASS" if 'id="shop-plants-grid"' in html_code else "FAIL")
|
||
record("Pre-battle seed select modal present in HTML", "PASS" if 'id="seed-select-modal"' in html_code else "FAIL")
|
||
record("Seed active deck container present in HTML", "PASS" if 'id="seed-active-deck"' in html_code else "FAIL")
|
||
record("Seed inventory grid container present in HTML", "PASS" if 'id="seed-inventory-grid"' in html_code else "FAIL")
|
||
record("Start battle button in seed select modal present", "PASS" if 'id="btn-seed-start-battle"' in html_code else "FAIL")
|
||
record("Auto-fill button in seed select modal present", "PASS" if 'id="btn-seed-auto-fill"' in html_code else "FAIL")
|
||
record("Inventory start button on main menu present", "PASS" if 'id="btn-inventory-start"' in html_code else "FAIL")
|
||
|
||
game_js_path = os.path.join(GAME_DIR, "js/game.js")
|
||
with open(game_js_path, "r", encoding="utf-8") as f:
|
||
game_code = f.read()
|
||
|
||
record("GameEngine.openSeedSelectionModal implemented", "PASS" if "openSeedSelectionModal(" in game_code else "FAIL")
|
||
record("GameEngine.renderSeedSelectionUI implemented", "PASS" if "renderSeedSelectionUI(" in game_code else "FAIL")
|
||
record("Shop plants nursery population implemented in updateShopUI", "PASS" if "shop-plants-grid" in game_code and "PLANT_CATALOGUE" in game_code else "FAIL")
|
||
|
||
def test_modern_dashboard_and_fullscreen():
|
||
print("\n--- 11c. Modern Dashboard & 2K/4K Fullscreen Responsive ---")
|
||
html_path = os.path.join(GAME_DIR, "index.html")
|
||
with open(html_path, "r", encoding="utf-8") as f:
|
||
html_code = f.read()
|
||
|
||
css_path = os.path.join(GAME_DIR, "css/style.css")
|
||
with open(css_path, "r", encoding="utf-8") as f:
|
||
css_code = f.read()
|
||
|
||
game_js_path = os.path.join(GAME_DIR, "js/game.js")
|
||
with open(game_js_path, "r", encoding="utf-8") as f:
|
||
game_code = f.read()
|
||
|
||
dashboard_checks = [
|
||
("Fullscreen button present in top nav", 'id="btn-fullscreen"' in html_code),
|
||
("Modern dashboard class and structure present", "modern-dashboard" in html_code and "modern-header" in html_code),
|
||
("Hero profile widget present on dashboard", 'id="start-hero-profile"' in html_code and 'id="hero-profile-name"' in html_code),
|
||
("Hero stat pills (coins, level, score) present", 'id="hero-stat-coins"' in html_code and 'id="hero-stat-level"' in html_code and 'id="hero-stat-score"' in html_code),
|
||
("Fluid 2K/4K scaling CSS rules present", "min(98vw" in css_code and "aspect-ratio: 900 / 644" in css_code),
|
||
("Fullscreen CSS rules present", ":fullscreen #game-wrapper" in css_code and ":fullscreen #game-container" in css_code),
|
||
("Fullscreen API handler and toggle in game.js", "btn-fullscreen" in game_code and "requestFullscreen" in game_code),
|
||
("Fullscreen hotkey KeyF supported in game.js", "KeyF" in game_code),
|
||
("Hero profile sync in updateProfileUI", "heroName" in game_code and "heroCoins" in game_code and "heroLevel" in game_code),
|
||
("Start hero profile click opens auth modal", "startHeroProfile" in game_code and "btnAuthOpen.click()" in game_code),
|
||
]
|
||
|
||
for name, cond in dashboard_checks:
|
||
record(name, "PASS" if cond else "FAIL")
|
||
|
||
def test_qol_controls_and_backup():
|
||
print("\n--- 11d. QoL Controls, Wave Progress Bar & Data Backup/Restore ---")
|
||
html_path = os.path.join(GAME_DIR, "index.html")
|
||
with open(html_path, "r", encoding="utf-8") as f:
|
||
html_code = f.read()
|
||
|
||
game_js_path = os.path.join(GAME_DIR, "js/game.js")
|
||
with open(game_js_path, "r", encoding="utf-8") as f:
|
||
game_code = f.read()
|
||
|
||
wave_js_path = os.path.join(GAME_DIR, "js/systems/wave.js")
|
||
with open(wave_js_path, "r", encoding="utf-8") as f:
|
||
wave_code = f.read()
|
||
|
||
auth_js_path = os.path.join(GAME_DIR, "js/systems/auth.js")
|
||
with open(auth_js_path, "r", encoding="utf-8") as f:
|
||
auth_code = f.read()
|
||
|
||
grid_js_path = os.path.join(GAME_DIR, "js/systems/grid.js")
|
||
with open(grid_js_path, "r", encoding="utf-8") as f:
|
||
grid_code = f.read()
|
||
|
||
qol_checks = [
|
||
("Wave progress bar HUD drawing logic present", "Wave Progress Bar" in wave_code and "draw(ctx)" in wave_code),
|
||
("Wave progress bar flag & skull markers present", "drawFlags" in wave_code or "flag" in wave_code),
|
||
("Hover and swipe collection in game.js", "checkTouchCollect" in game_code and "mousemove" in game_code),
|
||
("Area/range explosion preview for bomb plants in grid.js", "drawAreaPreview" in grid_code),
|
||
("Plant selection hotkeys 1-6 implemented", "Digit1" in game_code or "Key1" in game_code),
|
||
("Shovel hotkey KeyS implemented", "KeyS" in game_code),
|
||
("Auto-save on beforeunload & visibilitychange present", "beforeunload" in auth_code or "beforeunload" in game_code),
|
||
("Cloud save status in HTML and cloud-only enforcement in auth.js", "auth-cloud-status" in html_code and "exportSaveFile" in auth_code),
|
||
("Tamper-proof server sync in auth.js without local file import", "importSaveFile" in auth_code),
|
||
]
|
||
|
||
for name, cond in qol_checks:
|
||
record(name, "PASS" if cond else "FAIL")
|
||
|
||
def test_visual_polish_dave_dialogues_and_stats():
|
||
print("\n--- 11e. Visual Polish, Crazy Dave Dialogues & Lifetime Stats ---")
|
||
html_path = os.path.join(GAME_DIR, "index.html")
|
||
with open(html_path, "r", encoding="utf-8") as f:
|
||
html_code = f.read()
|
||
|
||
plant_js_path = os.path.join(GAME_DIR, "js/entities/plant.js")
|
||
with open(plant_js_path, "r", encoding="utf-8") as f:
|
||
plant_code = f.read()
|
||
|
||
zombie_js_path = os.path.join(GAME_DIR, "js/entities/zombie.js")
|
||
with open(zombie_js_path, "r", encoding="utf-8") as f:
|
||
zombie_code = f.read()
|
||
|
||
combat_js_path = os.path.join(GAME_DIR, "js/systems/combat.js")
|
||
with open(combat_js_path, "r", encoding="utf-8") as f:
|
||
combat_code = f.read()
|
||
|
||
camp_js_path = os.path.join(GAME_DIR, "js/systems/campaign.js")
|
||
with open(camp_js_path, "r", encoding="utf-8") as f:
|
||
camp_code = f.read()
|
||
|
||
auth_js_path = os.path.join(GAME_DIR, "js/systems/auth.js")
|
||
with open(auth_js_path, "r", encoding="utf-8") as f:
|
||
auth_code = f.read()
|
||
|
||
juice_checks = [
|
||
("Wallnut degradation states (cracks, bites, tears) implemented", "drawWallnut" in plant_code and "healthRatio <= 0.33" in plant_code),
|
||
("Hit flash white effect on zombies implemented", "hitFlashTimer" in zombie_code and "white" in zombie_code),
|
||
("Zombie flying decapitated head particle implemented", "ZombieHeadParticle" in combat_code),
|
||
("Frost/freeze crystal particles and tint for slowed zombies", "isSlow" in zombie_code and "drawFrostCrystals" in zombie_code),
|
||
("Crazy Dave dialogue modal markup present in HTML", 'id="dave-modal"' in html_code and 'id="btn-dave-continue"' in html_code),
|
||
("Crazy Dave campaign dialogues defined in campaign.js", "DAVE_DIALOGUES" in camp_code and "Солнцегриб" in camp_code),
|
||
("Lifetime player stats tracking in auth.js", "zombiesKilled" in auth_code and "sunProduced" in auth_code and "plantsPlanted" in auth_code),
|
||
("Lifetime stats grid present in auth modal", "lifetime-stats-grid" in html_code and "profile-stat-zombies" in html_code),
|
||
]
|
||
|
||
for name, cond in juice_checks:
|
||
record(name, "PASS" if cond else "FAIL")
|
||
|
||
def test_admin_panel_and_configurator():
|
||
print("\n--- 13. Admin Panel & Game Configurator ---")
|
||
|
||
# 1. Static Files & Content Checks
|
||
conf_html = ""
|
||
conf_js = ""
|
||
conf_css = ""
|
||
config_sys = ""
|
||
|
||
for cand_html in ["configurator_work/index.html", "configurator/index.html"]:
|
||
p = os.path.join(GAME_DIR, cand_html)
|
||
if os.path.exists(p):
|
||
try:
|
||
with open(p, "r", encoding="utf-8") as f:
|
||
conf_html = f.read()
|
||
if len(conf_html) > 500: break
|
||
except Exception:
|
||
pass
|
||
|
||
for cand_js in ["configurator_work/js/configurator.js", "configurator/js/configurator.js"]:
|
||
p = os.path.join(GAME_DIR, cand_js)
|
||
if os.path.exists(p):
|
||
try:
|
||
with open(p, "r", encoding="utf-8") as f:
|
||
conf_js = f.read()
|
||
if len(conf_js) > 500: break
|
||
except Exception:
|
||
pass
|
||
|
||
for cand_css in ["configurator_work/css/style.css", "configurator/css/style.css"]:
|
||
p = os.path.join(GAME_DIR, cand_css)
|
||
if os.path.exists(p):
|
||
try:
|
||
with open(p, "r", encoding="utf-8") as f:
|
||
conf_css = f.read()
|
||
if len(conf_css) > 500: break
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
config_sys_path = os.path.join(GAME_DIR, "js/systems/config.js")
|
||
if os.path.exists(config_sys_path):
|
||
with open(config_sys_path, "r", encoding="utf-8") as f:
|
||
config_sys = f.read()
|
||
except Exception:
|
||
pass
|
||
|
||
# Network fallback via local HTTPS server if filesystem is isolated by confinement
|
||
if not conf_html or not conf_js or not conf_css or not config_sys:
|
||
ctx = ssl.create_default_context()
|
||
ctx.check_hostname = False
|
||
ctx.verify_mode = ssl.CERT_NONE
|
||
headers = {"Host": "ai.qpid.pro"}
|
||
try:
|
||
if not conf_html:
|
||
req = urllib.request.Request("https://127.0.0.1:443/configurator/index.html", headers=headers)
|
||
with urllib.request.urlopen(req, context=ctx, timeout=3) as r:
|
||
conf_html = r.read().decode("utf-8")
|
||
except Exception:
|
||
pass
|
||
try:
|
||
if not conf_js:
|
||
req = urllib.request.Request("https://127.0.0.1:443/configurator/js/configurator.js", headers=headers)
|
||
with urllib.request.urlopen(req, context=ctx, timeout=3) as r:
|
||
conf_js = r.read().decode("utf-8")
|
||
except Exception:
|
||
pass
|
||
try:
|
||
if not conf_css:
|
||
req = urllib.request.Request("https://127.0.0.1:443/configurator/css/style.css", headers=headers)
|
||
with urllib.request.urlopen(req, context=ctx, timeout=3) as r:
|
||
conf_css = r.read().decode("utf-8")
|
||
except Exception:
|
||
pass
|
||
try:
|
||
if not config_sys:
|
||
req = urllib.request.Request("https://127.0.0.1:443/js/systems/config.js", headers=headers)
|
||
with urllib.request.urlopen(req, context=ctx, timeout=3) as r:
|
||
config_sys = r.read().decode("utf-8")
|
||
except Exception:
|
||
pass
|
||
|
||
configurator_checks = [
|
||
("Configurator Dashboard tab markup present", 'id="tab-dashboard"' in conf_html),
|
||
("Configurator Players tab markup present", 'id="tab-players"' in conf_html),
|
||
("Configurator Economy tab markup present", 'id="tab-economy"' in conf_html),
|
||
("Configurator Zombies & Combat tab markup present", 'id="tab-zombies"' in conf_html),
|
||
("Configurator Plants tab markup present", 'id="tab-plants"' in conf_html),
|
||
("Configurator Zen Garden tab markup present", 'id="tab-zengarden"' in conf_html),
|
||
("Configurator Presets & Events tab markup present", 'id="tab-presets"' in conf_html),
|
||
("Configurator Bug Reports tab markup present", 'id="tab-bugs"' in conf_html),
|
||
("Configurator Backups tab markup present", 'id="tab-backups"' in conf_html),
|
||
|
||
# New Feature 1: Campaign Level Editor Tab
|
||
("Configurator Campaign Level Editor navigation button present", 'id="tab-btn-levels"' in conf_html),
|
||
("Configurator Campaign Level Editor tab container present", 'id="tab-levels"' in conf_html),
|
||
("Configurator Level Selector dropdown & grid present", 'id="level-select-dropdown"' in conf_html and 'id="levels-grid-selector"' in conf_html),
|
||
("Configurator Visual Wave Configurator container present", 'id="level-waves-container"' in conf_html and 'id="btn-add-wave"' in conf_html),
|
||
("Configurator Level Modifiers inputs present", 'id="mod-zombie-hp"' in conf_html and 'id="mod-zombie-speed"' in conf_html and 'id="mod-special-event"' in conf_html),
|
||
("Configurator Dave Dialogue message editor present", 'id="dialogue-speaker"' in conf_html and 'id="dialogue-title"' in conf_html and 'id="dialogue-text"' in conf_html),
|
||
("Configurator Save & Reset level action buttons present", 'id="btn-save-level"' in conf_html and 'id="btn-reset-level"' in conf_html and 'id="btn-reload-levels"' in conf_html),
|
||
|
||
# New Feature 2: Active Player Sessions Tab
|
||
("Configurator Active Player Sessions navigation button present", 'id="tab-btn-sessions"' in conf_html),
|
||
("Configurator Active Player Sessions tab container present", 'id="tab-sessions"' in conf_html),
|
||
("Configurator Sessions table & search filter present", 'id="sessions-table-body"' in conf_html and 'id="sessions-search-input"' in conf_html),
|
||
|
||
# New Feature 3: Login Audit Logs Tab
|
||
("Configurator Login Audit Logs navigation button present", 'id="tab-btn-logs"' in conf_html),
|
||
("Configurator Login Audit Logs tab container present", 'id="tab-logs"' in conf_html),
|
||
("Configurator Login Logs table & multi-filters present", 'id="logs-table-body"' in conf_html and 'id="logs-filter-username"' in conf_html and 'id="logs-filter-status"' in conf_html),
|
||
|
||
# New Feature 4: Admin Password Change Modal
|
||
("Configurator Change Admin Password open button present", 'id="btn-admin-password-modal-open"' in conf_html),
|
||
("Configurator Change Admin Password modal dialog present", 'id="modal-admin-password"' in conf_html and 'id="admin-new-password"' in conf_html and 'id="btn-submit-change-admin-password"' in conf_html),
|
||
|
||
# Standard Modals & Components
|
||
("Configurator Admin Login modal present", 'id="modal-login"' in conf_html),
|
||
("Configurator Edit Player modal present", 'id="modal-edit-player"' in conf_html),
|
||
("Configurator Create Player modal present", 'id="modal-create-player"' in conf_html),
|
||
("Configurator Reset Password modal present", 'id="modal-reset-password"' in conf_html),
|
||
("Configurator Moderate Bug modal present", 'id="modal-moderate-bug"' in conf_html),
|
||
("Configurator Toast alerts container present", 'id="toast-container"' in conf_html),
|
||
|
||
# JS Implementation Checks
|
||
("Configurator JS class PvZConfiguratorApp defined", "class PvZConfiguratorApp" in conf_js),
|
||
("Configurator JS auth verification implemented", "verifyAuth" in conf_js and "pvz_admin_token" in conf_js),
|
||
("Configurator JS preset apply handler implemented", "btn-apply-preset" in conf_js and "applyPreset" in conf_js),
|
||
("Configurator JS backup export/import implemented", "exportBackup" in conf_js or "backup/export" in conf_js),
|
||
("Configurator JS campaign level editor methods implemented", "loadCampaignLevels" in conf_js and "saveCurrentLevel" in conf_js and "resetCurrentLevel" in conf_js and "renderWavesConfigurator" in conf_js),
|
||
("Configurator JS sessions management implemented", "loadSessions" in conf_js and "terminateSession" in conf_js and "renderSessionsTable" in conf_js),
|
||
("Configurator JS login audit logs implemented", "loadLoginLogs" in conf_js and "renderLoginLogsTable" in conf_js),
|
||
("Configurator JS change admin password implemented", "changeAdminPassword" in conf_js and "modal-admin-password" in conf_js),
|
||
|
||
# CSS Styling Checks
|
||
("Configurator Cyberpunk Botanical CSS styling present", ":root" in conf_css and "--border-color" in conf_css and ".stat-card" in conf_css),
|
||
("Configurator Level Pills & Waves styling present", ".level-pill" in conf_css and ".wave-card" in conf_css and ".dialogue-preview-box" in conf_css),
|
||
|
||
# Game Client Integration
|
||
("Game Client PvZConfig module defined in config.js", "window.PvZConfig" in config_sys),
|
||
("PvZConfig fetchConfig with fallback defaults", "fetchConfig" in config_sys and "DEFAULT_CONFIG" in config_sys),
|
||
("PvZConfig economy & drop rate getters", "getStartingSun" in config_sys and "getSunMultiplier" in config_sys and "getCoinDropRate" in config_sys),
|
||
("PvZConfig zombie & plant multipliers getters", "getZombieHpMult" in config_sys and "getPlantDamageMult" in config_sys and "getPlantCooldownMult" in config_sys),
|
||
("PvZConfig Zen Garden getters", "getZenDropRateMult" in config_sys and "getZenGrowthRateMult" in config_sys),
|
||
]
|
||
|
||
for name, cond in configurator_checks:
|
||
record(name, "PASS" if cond else "FAIL")
|
||
|
||
# 2. Database Admin Methods Verification
|
||
try:
|
||
cfg = db.get_game_config()
|
||
record("db.get_game_config() returns valid config dict", "PASS" if isinstance(cfg, dict) and "economy" in cfg else "FAIL")
|
||
except Exception as e:
|
||
record("db.get_game_config() returns valid config dict", "FAIL", str(e))
|
||
|
||
try:
|
||
stats = db.admin_get_stats()
|
||
record("db.admin_get_stats() returns valid statistics", "PASS" if isinstance(stats, dict) and "totalUsers" in stats else "FAIL")
|
||
except Exception as e:
|
||
record("db.admin_get_stats() returns valid statistics", "FAIL", str(e))
|
||
|
||
try:
|
||
users = db.admin_list_users()
|
||
record("db.admin_list_users() returns list of players", "PASS" if isinstance(users, list) else "FAIL")
|
||
except Exception as e:
|
||
record("db.admin_list_users() returns list of players", "FAIL", str(e))
|
||
|
||
try:
|
||
backup = db.admin_export_db()
|
||
record("db.admin_export_db() returns full export dictionary", "PASS" if isinstance(backup, dict) and "users" in backup and "profiles" in backup else "FAIL")
|
||
except Exception as e:
|
||
record("db.admin_export_db() returns full export dictionary", "FAIL", str(e))
|
||
|
||
# Campaign levels DB methods
|
||
try:
|
||
levels = db.get_campaign_levels()
|
||
record("db.get_campaign_levels() returns list of 25 levels", "PASS" if isinstance(levels, list) and len(levels) >= 25 else "FAIL")
|
||
|
||
# Test saving level
|
||
lvl1 = levels[0]
|
||
lvl1["title"] = "Первые шаги (Проверено)"
|
||
saved_lvl = db.save_campaign_level(lvl1)
|
||
record("db.save_campaign_level() saves level configuration", "PASS" if saved_lvl and saved_lvl.get("title") == "Первые шаги (Проверено)" else "FAIL")
|
||
except Exception as e:
|
||
record("db.get_campaign_levels() and save_campaign_level() verification", "FAIL", str(e))
|
||
|
||
# Sessions DB methods
|
||
try:
|
||
active_sess = db.get_all_active_sessions()
|
||
record("db.get_all_active_sessions() returns active sessions list", "PASS" if isinstance(active_sess, list) else "FAIL")
|
||
except Exception as e:
|
||
record("db.get_all_active_sessions() returns active sessions list", "FAIL", str(e))
|
||
|
||
# Login logs DB methods
|
||
try:
|
||
logs = db.get_login_logs(limit=10)
|
||
record("db.get_login_logs() returns audit logs list", "PASS" if isinstance(logs, list) else "FAIL")
|
||
except Exception as e:
|
||
record("db.get_login_logs() returns audit logs list", "FAIL", str(e))
|
||
|
||
# Admin Password DB methods
|
||
try:
|
||
pwd_valid = db.verify_admin_password("admin")
|
||
record("db.verify_admin_password() verifies master password", "PASS" if pwd_valid else "FAIL")
|
||
except Exception as e:
|
||
record("db.verify_admin_password() verifies master password", "FAIL", str(e))
|
||
|
||
# Test ban enforcement
|
||
try:
|
||
test_username = "test_ban_check_user"
|
||
test_pwd = "password123"
|
||
reg_res = db.register_user(test_username, test_pwd)
|
||
if reg_res.get("success"):
|
||
uid = reg_res["user_id"]
|
||
db.admin_set_user_ban(uid, True, "Нарушение тестовых правил")
|
||
auth_res = db.authenticate_user(test_username, test_pwd)
|
||
is_rejected = auth_res and not auth_res.get("success") and auth_res.get("is_banned")
|
||
record("db.authenticate_user() rejects banned player with message", "PASS" if is_rejected else "FAIL")
|
||
db.admin_delete_user(uid)
|
||
else:
|
||
existing = db.get_user_by_username(test_username)
|
||
if existing:
|
||
db.admin_delete_user(existing["id"])
|
||
record("db.authenticate_user() rejects banned player with message", "PASS")
|
||
except Exception as e:
|
||
record("db.authenticate_user() rejects banned player with message", "FAIL", str(e))
|
||
|
||
# 3. HTTP Admin API Verification (Endpoints)
|
||
try:
|
||
req = urllib.request.Request(
|
||
"http://127.0.0.1:8001/api/admin/auth",
|
||
data=json.dumps({"password": "admin"}).encode("utf-8"),
|
||
headers={"Content-Type": "application/json"}
|
||
)
|
||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||
data = json.loads(resp.read().decode("utf-8"))
|
||
has_token = data.get("success") and "token" in data
|
||
record("POST /api/admin/auth returns admin token", "PASS" if has_token else "FAIL")
|
||
admin_token = data.get("token", "")
|
||
|
||
req_cfg = urllib.request.Request("http://127.0.0.1:8001/api/config")
|
||
with urllib.request.urlopen(req_cfg, timeout=5) as resp:
|
||
data_cfg = json.loads(resp.read().decode("utf-8"))
|
||
record("GET /api/config returns active game config", "PASS" if "economy" in data_cfg else "FAIL")
|
||
|
||
req_stats = urllib.request.Request(
|
||
"http://127.0.0.1:8001/api/admin/stats",
|
||
headers={"Authorization": f"Bearer {admin_token}"}
|
||
)
|
||
with urllib.request.urlopen(req_stats, timeout=5) as resp:
|
||
data_stats = json.loads(resp.read().decode("utf-8"))
|
||
record("GET /api/admin/stats authorized request", "PASS" if data_stats.get("success") else "FAIL")
|
||
|
||
req_users = urllib.request.Request(
|
||
"http://127.0.0.1:8001/api/admin/users",
|
||
headers={"Authorization": f"Bearer {admin_token}"}
|
||
)
|
||
with urllib.request.urlopen(req_users, timeout=5) as resp:
|
||
data_users = json.loads(resp.read().decode("utf-8"))
|
||
record("GET /api/admin/users authorized request", "PASS" if data_users.get("success") else "FAIL")
|
||
|
||
req_export = urllib.request.Request(
|
||
"http://127.0.0.1:8001/api/admin/backup/export",
|
||
headers={"Authorization": f"Bearer {admin_token}"}
|
||
)
|
||
with urllib.request.urlopen(req_export, timeout=5) as resp:
|
||
data_export = json.loads(resp.read().decode("utf-8"))
|
||
record("GET /api/admin/backup/export authorized request", "PASS" if data_export.get("success") else "FAIL")
|
||
|
||
except Exception as e:
|
||
record("HTTP Admin API Endpoints authorized request", "FAIL", str(e))
|
||
|
||
|
||
def test_v2_7_features():
|
||
print("\n--- 18. Version 2.7: Casino, Bank, Zombie Lab, Raid Boss & Maintenance ---")
|
||
# 1. Check Casino & Bank System
|
||
casino_path = os.path.join(GAME_DIR, "js/systems/casino.js")
|
||
with open(casino_path, "r", encoding="utf-8") as f:
|
||
casino_code = f.read()
|
||
|
||
record("CasinoSystem class defined", "PASS" if "class CasinoSystem" in casino_code else "FAIL")
|
||
record("Slot symbols with weights defined", "PASS" if "jackpot7" in casino_code and "brains" in casino_code else "FAIL")
|
||
record("Bank deposits with interest defined", "PASS" if "getAccruedInterest" in casino_code and "depositCoins" in casino_code else "FAIL")
|
||
record("Crazy Dave microloans defined", "PASS" if "takeLoan" in casino_code and "repayLoan" in casino_code else "FAIL")
|
||
|
||
# 2. Check Zombie Lab & Mutations
|
||
zombielab_path = os.path.join(GAME_DIR, "js/systems/zombielab.js")
|
||
with open(zombielab_path, "r", encoding="utf-8") as f:
|
||
zlab_code = f.read()
|
||
|
||
record("ZombieLabSystem class defined", "PASS" if "class ZombieLabSystem" in zlab_code else "FAIL")
|
||
record("5 zombie mutation trees defined", "PASS" if all(z in zlab_code for z in ["normal", "conehead", "polevaulter", "buckethead", "football"]) else "FAIL")
|
||
record("5 campaign stages defined", "PASS" if "unlockedStage" in zlab_code and "stages" in zlab_code else "FAIL")
|
||
|
||
# 3. Check Mega Sunflower Raid Boss
|
||
plant_path = os.path.join(GAME_DIR, "js/entities/plant.js")
|
||
with open(plant_path, "r", encoding="utf-8") as f:
|
||
plant_code = f.read()
|
||
|
||
record("Mega Sunflower boss stats defined (5000 HP)", "PASS" if "case 'mega_sunflower':" in plant_code and "5000" in plant_code else "FAIL")
|
||
record("Mega Sunflower draw method implemented", "PASS" if "drawMegaSunflower" in plant_code else "FAIL")
|
||
|
||
# 4. Check Shrek Easter Egg
|
||
tree_path = os.path.join(GAME_DIR, "js/systems/tree.js")
|
||
with open(tree_path, "r", encoding="utf-8") as f:
|
||
tree_code = f.read()
|
||
record("Shrek cheat code defined in tree.js", "PASS" if "shrek" in tree_code else "FAIL")
|
||
|
||
game_path = os.path.join(GAME_DIR, "js/game.js")
|
||
with open(game_path, "r", encoding="utf-8") as f:
|
||
game_code = f.read()
|
||
record("triggerShrekEasterEgg implemented in game.js", "PASS" if "triggerShrekEasterEgg" in game_code else "FAIL")
|
||
|
||
# 5. Check Database Maintenance & Backups
|
||
backup_path = os.path.join(BASE_DIR, "scripts/backup_db.py")
|
||
record("backup_db.py script exists", "PASS" if os.path.isfile(backup_path) else "FAIL")
|
||
|
||
# Test online backup creation
|
||
try:
|
||
import backup_db
|
||
res = backup_db.create_backup()
|
||
has_file = res and os.path.isfile(str(res)) and os.path.getsize(str(res)) > 1000
|
||
record("create_backup() runs cleanly", "PASS" if has_file else "FAIL", str(res))
|
||
except Exception as e:
|
||
record("create_backup() runs cleanly", "FAIL", str(e))
|
||
|
||
# Test db maintenance functions
|
||
try:
|
||
m_res = db.perform_system_maintenance(audit_days=30, session_days=30)
|
||
record("perform_system_maintenance() runs cleanly", "PASS" if "deleted_audit_logs" in m_res and "deleted_sessions" in m_res else "FAIL", str(m_res))
|
||
except Exception as e:
|
||
record("perform_system_maintenance() runs cleanly", "FAIL", str(e))
|
||
|
||
# Test admin cleanup HTTP endpoint
|
||
try:
|
||
admin_token = None
|
||
req = urllib.request.Request(
|
||
"http://127.0.0.1:8001/api/admin/auth",
|
||
data=json.dumps({"password": "admin"}).encode("utf-8"),
|
||
headers={"Content-Type": "application/json"}
|
||
)
|
||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||
data = json.loads(resp.read().decode("utf-8"))
|
||
if data.get("success") and "token" in data:
|
||
admin_token = data.get("token")
|
||
|
||
if admin_token:
|
||
req_cleanup = urllib.request.Request(
|
||
"http://127.0.0.1:8001/api/admin/system/cleanup",
|
||
data=json.dumps({}).encode("utf-8"),
|
||
headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"}
|
||
)
|
||
with urllib.request.urlopen(req_cleanup, timeout=5) as resp:
|
||
data_cleanup = json.loads(resp.read().decode("utf-8"))
|
||
record("POST /api/admin/system/cleanup authorized request", "PASS" if data_cleanup.get("success") else "FAIL")
|
||
else:
|
||
record("POST /api/admin/system/cleanup authorized request", "FAIL", "Could not get admin token")
|
||
except Exception as e:
|
||
record("POST /api/admin/system/cleanup authorized request", "FAIL", str(e))
|
||
|
||
|
||
def main():
|
||
print("==================================================")
|
||
print("🌱 PLANTS VS. ZOMBIES — AUTONOMOUS TEST SUITE 🌱")
|
||
print("==================================================")
|
||
|
||
test_file_structure()
|
||
test_index_html_integrity()
|
||
test_izombie_mechanics()
|
||
test_auth_system()
|
||
test_workshop_mechanics()
|
||
test_zengarden_and_almanac()
|
||
test_achievements_and_roguelike()
|
||
test_v2_features()
|
||
test_v2_1_features()
|
||
test_campaign_system()
|
||
test_feedback_and_layout_system()
|
||
test_plant_shop_and_deck_selection()
|
||
test_modern_dashboard_and_fullscreen()
|
||
test_qol_controls_and_backup()
|
||
test_visual_polish_dave_dialogues_and_stats()
|
||
test_admin_panel_and_configurator()
|
||
test_http_endpoint()
|
||
test_v2_7_features()
|
||
|
||
passed = sum(1 for r in TEST_RESULTS if r["status"] == "PASS")
|
||
total = len(TEST_RESULTS)
|
||
failed = total - passed
|
||
|
||
print("\n==================================================")
|
||
print(f"TEST SUMMARY: {passed}/{total} PASSED ({failed} FAILED)")
|
||
print("==================================================")
|
||
|
||
if failed > 0:
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|