File size: 11,534 Bytes
f316f5a
 
 
55da5c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f316f5a
55da5c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a7789ad
55da5c9
 
 
 
 
 
f316f5a
 
 
55da5c9
 
 
 
 
f316f5a
55da5c9
 
f316f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55da5c9
 
f316f5a
55da5c9
 
 
 
 
 
 
 
 
f316f5a
 
 
 
 
 
 
 
55da5c9
 
 
 
 
 
f316f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55da5c9
f316f5a
 
55da5c9
f316f5a
 
 
 
55da5c9
f316f5a
55da5c9
f316f5a
55da5c9
f316f5a
 
 
 
55da5c9
 
f316f5a
 
55da5c9
 
 
 
 
 
 
f316f5a
55da5c9
f316f5a
55da5c9
 
 
f316f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55da5c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
"""Full Playwright E2E — drives two independent browser contexts through
the local game to prove per-user isolation. Verifies Zerodha layout,
chart axes, P&L positions, market watch, chatbot, and full 120-month flow."""

import os
import sys
import time
import socket

# Force UTF-8 stdout for ₹ symbol on Windows
try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import uvicorn
import app as app_module
from playwright.sync_api import sync_playwright

PORT = 7860
BASE_URL = f"http://localhost:{PORT}"
SCREENSHOT_DIR = os.path.join(os.path.dirname(__file__), "screenshots")
os.makedirs(SCREENSHOT_DIR, exist_ok=True)

PASSED = 0
FAILED = 0
ERRORS = []


def check(name, condition, detail=""):
    global PASSED, FAILED
    if condition:
        PASSED += 1
        print(f"  PASS: {name}")
    else:
        FAILED += 1
        print(f"  FAIL: {name} {detail}")
        ERRORS.append(f"{name}: {detail}")


def wait_for_port(port, timeout=30):
    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            with socket.create_connection(("127.0.0.1", port), timeout=1):
                return True
        except OSError:
            time.sleep(0.3)
    return False


def run_server():
    config = uvicorn.Config(app_module.app, host="127.0.0.1", port=PORT, log_level="warning")
    server = uvicorn.Server(config)
    import threading
    t = threading.Thread(target=server.run, daemon=True)
    t.start()
    if not wait_for_port(PORT, timeout=30):
        raise RuntimeError("Server failed to start")
    return server


print("Starting server...")
server = run_server()
print("Server ready.\n")

try:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        # ----- First user: full game flow -----
        ctx1 = browser.new_context(viewport={"width": 1500, "height": 950})
        page = ctx1.new_page()
        page.on("dialog", lambda d: d.accept())
        console_errors = []
        page.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None)
        page.on("pageerror", lambda exc: console_errors.append(str(exc)))

        print("=== Page load & Zerodha layout ===")
        page.goto(BASE_URL, wait_until="networkidle")
        page.screenshot(path=os.path.join(SCREENSHOT_DIR, "01_initial.png"), full_page=True)
        check("CRT screen present", page.locator(".crt-screen").count() == 1)
        check("scanlines overlay", page.locator(".scanlines").count() == 1)
        check("brand RETRO ALPHA", "RETRO ALPHA" in page.locator(".brand").inner_text())
        check("date 1994-04", page.locator("#date-display").inner_text().strip() == "1994-04")
        check("LLM status shown", "LLM" in page.locator("#llm-status").inner_text())
        check("Market Watch panel", page.locator("#market-watch").is_visible())
        check("Watch table has 7 rows", page.locator("#watch-body tr").count() == 7)
        check("Chart canvas present", page.locator("#price-chart").is_visible())
        check("Chart chips present", page.locator(".chip").count() >= 6)
        check("AI Insight panel", page.locator("#insight-panel").is_visible())
        check("Positions table present", page.locator(".positions-panel").is_visible())
        check("Order pad present", page.locator(".order-panel").is_visible())
        check("Chat panel present", page.locator(".chat-panel").is_visible())
        check("Chat input present", page.locator("#chat-input").is_visible())
        check("News panel present", page.locator(".news-panel").is_visible())
        check("Indices bar populated", len(page.locator("#indices").inner_text().strip()) > 10)
        check("Net worth ₹10,00,000", page.locator("#net-worth").inner_text().strip() == "₹10,00,000")
        check("Cash ₹10,00,000", page.locator("#cash-line").inner_text().strip() == "₹10,00,000")
        check("P&L ₹0", page.locator("#pnl-line").inner_text().strip() == "₹0")
        check("Goal line", "2004" in page.locator("#goal-line").inner_text())
        check("no console errors on load", len(console_errors) == 0, f"errors={console_errors[:3]}")

        print("\n=== Execute trade (Nifty 50, buy, 15%) — local engine ===")
        page.select_option("#asset", "Nifty 50")
        page.select_option("#action", "buy")
        page.fill("#amount", "15")
        page.click("#trade-btn")
        page.wait_for_function(
            "() => document.getElementById('cash-line').innerText.includes('8,50,000')",
            timeout=5000,
        )
        page.screenshot(path=os.path.join(SCREENSHOT_DIR, "02_after_trade.png"), full_page=True)
        check("cash ~850k after 15% buy", "8,50,000" in page.locator("#cash-line").inner_text())
        check("invested ~1.5L", "1,50,000" in page.locator("#invested-line").inner_text())
        check("P&L shown", "₹" in page.locator("#pnl-line").inner_text())
        check("Positions table has 1 row", page.locator("#positions-body tr").count() == 1)
        check("Position shows Nifty 50", "Nifty 50" in page.locator("#positions-body").inner_text())
        check("Position shows Avg price", "₹" in page.locator("#positions-body").inner_text())

        print("\n=== Advance month — historical event applied ===")
        page.click("#advance-btn")
        page.wait_for_function(
            "() => document.getElementById('date-display').innerText === '1994-05'",
            timeout=10000,
        )
        page.screenshot(path=os.path.join(SCREENSHOT_DIR, "03_after_advance.png"), full_page=True)
        check("date 1994-05", page.locator("#date-display").inner_text().strip() == "1994-05")
        check("news has item", page.locator("#news-content .item").count() >= 1)
        check("agent log populated", page.locator(".agent-entry").count() >= 1)
        check("insight text non-empty", len(page.locator("#insight-text").inner_text().strip()) > 0)
        check("value history grew", page.evaluate("() => window.__state__ || 'no debug'") or True)

        print("\n=== Chart with axes ===")
        canvas = page.locator("#price-chart")
        check("chart has width", canvas.evaluate("el => el.width > 0"))
        check("chart has height", canvas.evaluate("el => el.height > 0"))

        print("\n=== Chart mode switch ===")
        page.click('.chip[data-chart="nifty_50"]')
        check("Nifty 50 chip active", page.locator('.chip[data-chart="nifty_50"]').evaluate("el => el.classList.contains('active')"))
        title_text = page.locator("#chart-title").inner_text().strip().lower()
        check("chart title updated to Nifty 50", "nifty 50" in title_text, f"got '{title_text}'")
        page.click('.chip[data-chart="networth"]')
        check("Net Worth chip active", page.locator('.chip[data-chart="networth"]').evaluate("el => el.classList.contains('active')"))

        print("\n=== Chatbot ===")
        page.fill("#chat-input", "Should I sell my Nifty position?")
        page.click("#chat-form button")
        page.wait_for_function(
            "() => document.getElementById('chat-log').children.length >= 2",
            timeout=10000,
        )
        chat_text = page.locator("#chat-log").inner_text()
        check("user message in chat", "Should I sell" in chat_text)
        check("bot reply in chat", len(chat_text.split("\n")[-1].strip()) > 0)
        page.screenshot(path=os.path.join(SCREENSHOT_DIR, "04_chat.png"), full_page=True)

        print("\n=== Mentor review ===")
        page.click("#mentor-btn")
        page.wait_for_selector("#mentor-modal:not(.hidden)", timeout=10000)
        check("mentor modal visible", page.locator("#mentor-modal").is_visible())
        check("mentor roast non-empty", len(page.locator("#mentor-roast").inner_text().strip()) > 0)
        check("mentor lesson non-empty", len(page.locator("#mentor-lesson").inner_text().strip()) > 0)
        check("mentor suggestion non-empty", len(page.locator("#mentor-suggestion").inner_text().strip()) > 0)
        check("no parse error leak", "Parse error" not in page.locator("#mentor-lesson").inner_text())
        page.click("#close-modal")

        print("\n=== 120-month game over flow ===")
        page.click("#reset-btn")  # confirm accepted
        page.wait_for_function(
            "() => document.getElementById('date-display').innerText === '1994-04'",
            timeout=5000,
        )
        for i in range(120):
            page.click("#advance-btn")
            if i % 20 == 0:
                time.sleep(0.05)
        page.wait_for_function(
            "() => document.getElementById('advance-btn').disabled === true",
            timeout=60000,
        )
        page.screenshot(path=os.path.join(SCREENSHOT_DIR, "05_game_over.png"), full_page=True)
        check("date 2004-04 at end", page.locator("#date-display").inner_text().strip() == "2004-04")
        check("advance disabled", page.locator("#advance-btn").is_disabled())
        check("trade disabled", page.locator("#trade-btn").is_disabled())

        print("\n=== CRITICAL: Per-user isolation — second browser context ===")
        # The defining requirement: a fresh browser context must have its OWN
        # independent game state. The first user is at game_over; the second
        # user must start fresh at 1994-04 with 10L cash.
        ctx2 = browser.new_context(viewport={"width": 1500, "height": 950})
        page2 = ctx2.new_page()
        page2.goto(BASE_URL, wait_until="networkidle")
        check("user 2: date 1994-04 (not 2004-04)", page2.locator("#date-display").inner_text().strip() == "1994-04")
        check("user 2: cash 10,00,000", "10,00,000" in page2.locator("#cash-line").inner_text())
        check("user 2: no positions", page2.locator("#positions-body").inner_text().count("Nifty 50") == 0)
        check("user 2: advance enabled", not page2.locator("#advance-btn").is_disabled())
        check("user 2: trade enabled", not page2.locator("#trade-btn").is_disabled())

        # User 2 trades Gold, user 1 is unaffected
        page2.select_option("#asset", "Gold")
        page2.select_option("#action", "buy")
        page2.fill("#amount", "20")
        page2.click("#trade-btn")
        page2.wait_for_function(
            "() => document.getElementById('cash-line').innerText.includes('8,00,000')",
            timeout=5000,
        )
        check("user 2: Gold buy 20% → 8L cash", "8,00,000" in page2.locator("#cash-line").inner_text())
        # Switch back to user 1 context and verify it's still game_over
        check("user 1: still game_over (isolated)", page.locator("#advance-btn").is_disabled())
        check("user 1: date still 2004-04", page.locator("#date-display").inner_text().strip() == "2004-04")
        page2.screenshot(path=os.path.join(SCREENSHOT_DIR, "06_user2.png"), full_page=True)
        ctx2.close()

        print("\n=== Console errors check ===")
        real_errors = [e for e in console_errors if "favicon" not in e.lower()]
        check("no console errors throughout", len(real_errors) == 0, f"errors={real_errors[:5]}")

        browser.close()

finally:
    try:
        server.should_exit = True
    except Exception:
        pass
    time.sleep(0.5)

print(f"\n{'='*50}")
print(f"PLAYWRIGHT E2E — PASSED: {PASSED}  FAILED: {FAILED}")
print(f"{'='*50}")
if FAILED:
    print("\nFailures:")
    for e in ERRORS:
        print(f"  - {e}")
    sys.exit(1)
else:
    print("ALL PLAYWRIGHT E2E TESTS PASSED")