2026-08-30 10:55:29 +07:00
|
|
|
"""
|
|
|
|
|
Reproduce quiz option-display bug via Playwright.
|
|
|
|
|
- Login via UI navbar (token guru 141214)
|
|
|
|
|
- Buka /lesson/quiz_test
|
|
|
|
|
- Mulai kuis, navigasi bolak-balik antar soal (mcq <-> flashcard)
|
|
|
|
|
- Tiap soal mcq: bandingkan jumlah tombol .option-btn di DOM vs
|
|
|
|
|
panjang options dari server quiz_data (cocokkan by question text/prose)
|
|
|
|
|
- Log ke /tmp/repro_quiz.log
|
|
|
|
|
"""
|
2026-09-06 18:08:58 +07:00
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import urllib.request
|
2026-08-30 10:55:29 +07:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-09-06 18:08:58 +07:00
|
|
|
BASE = os.environ.get("ELEMES_TEST_BASE_URL", "https://sinau-c-dev.manakin-gentoo.ts.net")
|
|
|
|
|
TOKEN = os.environ.get("ELEMES_TEST_TOKEN", "141214")
|
|
|
|
|
SLUG = os.environ.get("ELEMES_TEST_SLUG", "quiz_test")
|
|
|
|
|
LOG = os.environ.get("ELEMES_TEST_LOG", "/tmp/repro_quiz.log")
|
2026-08-30 10:55:29 +07:00
|
|
|
|
|
|
|
|
def log(msg):
|
|
|
|
|
with open(LOG, "a") as f:
|
|
|
|
|
f.write(msg + "\n")
|
|
|
|
|
print(msg)
|
|
|
|
|
|
|
|
|
|
def norm(t):
|
|
|
|
|
return (t or "").replace("\n", " ").strip()[:50].lower()
|
|
|
|
|
|
|
|
|
|
def fetch_quiz_data():
|
|
|
|
|
url = f"{BASE}/api/lesson/{SLUG}.json?token={TOKEN}"
|
|
|
|
|
req = urllib.request.Request(url, headers={"User-Agent": "repro"})
|
|
|
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
|
|
|
data = json.load(r)
|
|
|
|
|
return data.get("quiz_data", [])
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
Path(LOG).write_text("")
|
|
|
|
|
try:
|
|
|
|
|
qd = fetch_quiz_data()
|
|
|
|
|
log(f"== server quiz_data: {len(qd)} soal")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log(f"!! gagal fetch quiz_data: {e}")
|
|
|
|
|
qd = []
|
|
|
|
|
|
|
|
|
|
server_by_text = {}
|
|
|
|
|
for q in qd:
|
|
|
|
|
txt = q.get("question") or q.get("front") or ""
|
|
|
|
|
server_by_text[norm(txt)] = len(q.get("options") or [])
|
|
|
|
|
|
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
|
with sync_playwright() as p:
|
|
|
|
|
b = p.chromium.launch(headless=True)
|
|
|
|
|
ctx = b.new_context(service_workers="block")
|
|
|
|
|
pg = ctx.new_page()
|
|
|
|
|
pg.on("console", lambda m: log(f"[console.{m.type}] {m.text}"))
|
|
|
|
|
pg.on("pageerror", lambda e: log(f"[pageerror] {e}"))
|
|
|
|
|
|
|
|
|
|
log(f"== goto {BASE}/lesson/{SLUG}")
|
|
|
|
|
pg.add_init_script(f"localStorage.setItem('student_token','{TOKEN}');")
|
|
|
|
|
pg.goto(f"{BASE}/lesson/{SLUG}", wait_until="networkidle", timeout=60000)
|
|
|
|
|
pg.wait_for_timeout(2000)
|
|
|
|
|
# reload biar auth.init() jalanin validateToken -> authLoggedIn=true
|
|
|
|
|
pg.reload(wait_until="networkidle")
|
|
|
|
|
pg.wait_for_timeout(2500)
|
|
|
|
|
|
|
|
|
|
# Buka modal login: klik dropdown-item "Masuk" lalu submit
|
|
|
|
|
try:
|
|
|
|
|
pg.locator(".dropdown-item", has_text="Masuk").first.click(timeout=8000)
|
|
|
|
|
pg.get_by_placeholder("Masukkan token...").fill(TOKEN)
|
|
|
|
|
pg.get_by_role("button", name="Masuk").click(timeout=8000)
|
|
|
|
|
log("== submit login modal OK")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log(f"!! gagal modal login: {e}")
|
|
|
|
|
# fallback: cari semua elemen ber-text Masuk
|
|
|
|
|
try:
|
|
|
|
|
els = pg.locator("text=Masuk").all_inner_texts()
|
|
|
|
|
log(f"text=Masuk elements: {els}")
|
|
|
|
|
except: pass
|
|
|
|
|
pg.wait_for_timeout(2000)
|
|
|
|
|
# debug: cek validateToken API dari browser context
|
|
|
|
|
vt = pg.evaluate("""async () => {
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch('/api/validate-token', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({token:'141214'})});
|
|
|
|
|
return await r.json();
|
|
|
|
|
} catch(e) { return 'ERR:'+e.message; }
|
|
|
|
|
}""")
|
|
|
|
|
log(f"== validateToken from browser: {vt}")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
pg.get_by_text("Mulai Kuis Sekarang").click(timeout=10000)
|
|
|
|
|
log("== klik Mulai Kuis OK")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log(f"!! gagal klik Mulai Kuis: {e}")
|
|
|
|
|
buttons = pg.locator("button").all_inner_texts()
|
|
|
|
|
log(f"buttons: {buttons[:20]}")
|
|
|
|
|
b.close()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
pg.wait_for_timeout(1500)
|
|
|
|
|
ndots = pg.locator(".nav-dot").count()
|
|
|
|
|
log(f"== jumlah nav-dot (soal): {ndots}")
|
|
|
|
|
|
|
|
|
|
mismatches = 0
|
|
|
|
|
for i in range(ndots):
|
|
|
|
|
pg.locator(".nav-dot").nth(i).click()
|
|
|
|
|
pg.wait_for_timeout(400)
|
|
|
|
|
opt_btns = pg.locator(".option-btn").count()
|
|
|
|
|
flash_ok = pg.locator(".btn-flashcard-ok").count()
|
|
|
|
|
q_prose = pg.locator(".quiz-question-prose").inner_text()[:50].replace("\n", " ")
|
|
|
|
|
expected = server_by_text.get(norm(q_prose))
|
|
|
|
|
tag = "flashcard" if flash_ok > 0 else ("mcq" if opt_btns > 0 else "EMPTY")
|
|
|
|
|
status = "OK"
|
|
|
|
|
if tag == "mcq" and expected is not None and opt_btns != expected:
|
|
|
|
|
status = f"!!! MISMATCH DOM={opt_btns} vs server={expected}"
|
|
|
|
|
mismatches += 1
|
|
|
|
|
elif tag == "mcq" and opt_btns == 0:
|
|
|
|
|
status = "!!! MCQ TANPA OPSI"
|
|
|
|
|
mismatches += 1
|
|
|
|
|
elif tag == "flashcard" and opt_btns > 0:
|
|
|
|
|
status = "!!! FLASHCARD PUNYA OPSI (aneh)"
|
|
|
|
|
mismatches += 1
|
|
|
|
|
elif tag == "EMPTY":
|
|
|
|
|
status = "!!! SOAL KOSONG (no option, no flashcard)"
|
|
|
|
|
mismatches += 1
|
|
|
|
|
log(f"[soal {i}] type={tag} prose={q_prose!r} DOM_opts={opt_btns} server_expected={expected} {status}")
|
|
|
|
|
|
|
|
|
|
log(f"== SELESAI. mismatches={mismatches}")
|
|
|
|
|
b.close()
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|