chore: tambah script debug scan/parse quiz & repro opsi via Playwright
_scan_quiz_issues.py: cari soal MCQ dengan <2 opsi (ter-split). repro_quiz_options.py: navigasi quiz di browser, assert jumlah opsi DOM vs server quiz_data.
This commit is contained in:
parent
a157a31b78
commit
17b645325c
|
|
@ -0,0 +1,24 @@
|
|||
import sys, glob
|
||||
sys.path.insert(0, '/home/a2nr/lms-dev/elemes')
|
||||
from services.lesson_service import _parse_flashcards, _extract_section
|
||||
|
||||
issues = 0
|
||||
for f in glob.glob('/home/a2nr/lms-dev/content/**/*.md', recursive=True):
|
||||
txt = open(f, encoding='utf-8').read()
|
||||
if '---QUIZ_FLASHCARD---' not in txt:
|
||||
continue
|
||||
raw, _ = _extract_section(txt, '---QUIZ_FLASHCARD---', '---END_QUIZ_FLASHCARD---')
|
||||
if not raw.strip():
|
||||
continue
|
||||
try:
|
||||
cards = _parse_flashcards(raw)
|
||||
except ValueError as e:
|
||||
print(f'[RAISE] {f}: {e}')
|
||||
issues += 1
|
||||
continue
|
||||
for c in cards:
|
||||
if c['type'] == 'mcq' and len(c.get('options') or []) < 2:
|
||||
print(f'[<2 OPSI] {f}: {c["question"][:60]!r} opsi={len(c.get("options") or [])}')
|
||||
issues += 1
|
||||
|
||||
print('ISSUES:', issues)
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
"""
|
||||
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
|
||||
"""
|
||||
import sys, json, urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
BASE = "https://sinau-c-dev.manakin-gentoo.ts.net"
|
||||
TOKEN = "141214"
|
||||
SLUG = "quiz_test"
|
||||
LOG = "/tmp/repro_quiz.log"
|
||||
|
||||
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()
|
||||
Loading…
Reference in New Issue