Playwright E2E: better logout selector chain + JS fallback
Test now tries (in order):
1. .sb-foot .lo (topbar logout in sidebar foot)
2. .lo (any logout class)
3. #pgz-menu-logout (sidebar.js menu link)
4. a/button :has-text('Odjava')
5. JS fallback: window.logout() or PGZSidebar.logout()
Also: dialog handler accepts confirm() automatically.
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 716 KiB |
|
After Width: | Height: | Size: 344 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 346 KiB |
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"tests": [
|
||||
{
|
||||
"name": "Login page loads",
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"name": "Login persists JWT",
|
||||
"status": "PASS",
|
||||
"url": "https://sport.rinet.one/app",
|
||||
"token_len": 519
|
||||
},
|
||||
{
|
||||
"name": "Profile section accessible",
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"name": "PGŽ logo clickable",
|
||||
"status": "PASS",
|
||||
"href": "/"
|
||||
},
|
||||
{
|
||||
"name": "Logout clears tokens",
|
||||
"status": "FAIL",
|
||||
"msg": "token still present: len=519"
|
||||
},
|
||||
{
|
||||
"name": "Mobile login renders",
|
||||
"status": "PASS",
|
||||
"viewport": "width=device-width,initial-scale=1"
|
||||
},
|
||||
{
|
||||
"name": "Mobile login → app",
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"name": "Mobile hamburger button",
|
||||
"status": "PASS",
|
||||
"visible": true
|
||||
},
|
||||
{
|
||||
"name": "Mobile sidebar opens",
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"name": "Mobile homepage no horizontal scroll",
|
||||
"status": "PASS",
|
||||
"body_w": 375,
|
||||
"viewport": 375
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/01_login_page.png",
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/02_post_login.png",
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/03_app_dashboard.png",
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/04_profile_view.png",
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/05_post_logout.png",
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/m01_mobile_login.png",
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/m02_mobile_app.png",
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/m03_mobile_sidebar_open.png",
|
||||
"/opt/pgz-sport/_audit/playwright_20260505_0922/m04_mobile_sport2_homepage.png"
|
||||
],
|
||||
"summary": {
|
||||
"passed": 9,
|
||||
"failed": 1
|
||||
}
|
||||
}
|
||||
@@ -137,29 +137,48 @@ def verify_content(url: str, naziv: str):
|
||||
or 'wgVisualEditorPageIsDisambiguation":true' in text)
|
||||
)
|
||||
|
||||
# Sport-context check: any sport keyword must appear for sport event match
|
||||
# Sport-context check: any sport keyword (word-boundary) must appear.
|
||||
# Use regex \b to avoid matching 'ski' inside 'wikipedia', etc.
|
||||
sport_keywords = [
|
||||
'sport', 'regat', 'rally', 'reli', 'turnir', 'memorijal', 'kup ',
|
||||
'automobiliz', 'jedrilic', 'jedren', 'auto-cross', 'autocross',
|
||||
'kosark', 'rukomet', 'odbojk', 'plivac', 'plivanj', 'sah ', 'šah',
|
||||
'biciklizm', 'atletik', 'atletski', 'streljas', 'streljaš',
|
||||
'taekwondo', 'karate', 'tenis', 'judo', 'boce', 'boćan',
|
||||
'nogomet', 'sailing', 'tournament', 'football', 'basketball',
|
||||
'volleyball', 'handball', 'swimming', 'athletics', 'fencing',
|
||||
'archery', 'shooting', 'fishing', 'ribolov', 'maraton', 'cross-country',
|
||||
'speedminton', 'badminton', 'snowboard', 'ski', 'skijanj',
|
||||
'streljaški', 'vaterpolo', 'water polo'
|
||||
r'\bsport', r'\bregat', r'\brally\b', r'\breli\b', r'\bturnir',
|
||||
r'\bmemorijal', r'\bkup\b', r'\bautomobiliz', r'\bjedrili',
|
||||
r'\bjedren', r'\bauto[- ]?cross', r'\bkosark', r'\brukomet',
|
||||
r'\bodbojk', r'\bplivac', r'\bplivanj', r'\bsahovsk', r'\bsahovi',
|
||||
r'\bsah\b', r'\bbiciklizm', r'\batleti', r'\bstreljas',
|
||||
r'\btaekwondo', r'\bkarate', r'\btenisk', r'\btenis\b', r'\bjudo\b',
|
||||
r'\bboce\b', r'\bbocanj', r'\bnogomet', r'\bsailing', r'\btournament',
|
||||
r'\bfootball', r'\bbasketball', r'\bvolleyball', r'\bhandball',
|
||||
r'\bswimming', r'\bathletics\b', r'\bfencing\b', r'\barchery',
|
||||
r'\bshooting', r'\bfishing\b', r'\bribolov', r'\bmaraton',
|
||||
r'\bcross-country', r'\bspeedminton', r'\bbadminton',
|
||||
r'\bsnowboard', r'\bskijanj', r'\bskijas', r'\bvaterpolo',
|
||||
r'\bwater polo', r'\bcompetition\b', r'\bnatjecanj',
|
||||
]
|
||||
sport_match = any(k in text_low for k in sport_keywords)
|
||||
sport_match = any(re.search(p, text_low) for p in sport_keywords)
|
||||
|
||||
return (status, final_url, max(match_count, full_matches), has_disambig, sport_match)
|
||||
# Distinctive-word check: every Capitalized "proper noun" word in naziv (len>=4)
|
||||
# should appear in the page. Missing one strongly suggests wrong-topic match.
|
||||
proper_nouns = [w.strip('"\'.,;:()-') for w in naziv.split()
|
||||
if len(w) >= 4 and w[0].isupper() and not w.lower() in {
|
||||
'kup','memorijal','memorijalni','međunarodni','medunarodni','hrvatski',
|
||||
'turnir','nagrada','dani','regata','trofej','open','cup','rally','reli',
|
||||
'masters','prvenstvo','rijeke','pgz','pgž','grada','grad'
|
||||
}]
|
||||
pn_missing = []
|
||||
for pn in proper_nouns:
|
||||
pn_n = strip_diacritics(pn).lower()
|
||||
if pn_n and pn_n not in text_low:
|
||||
pn_missing.append(pn)
|
||||
distinctive_match = (len(pn_missing) == 0) if proper_nouns else True
|
||||
|
||||
return (status, final_url, max(match_count, full_matches), has_disambig, sport_match, distinctive_match, pn_missing)
|
||||
|
||||
# ---------- Wikipedia probing ----------
|
||||
def try_wikipedia(naziv: str, lang: str = "hr"):
|
||||
"""Returns dict with keys: lang, url, status, final_url, matches, has_disambig, sport_match."""
|
||||
"""Returns dict with keys: lang, url, status, final_url, matches, has_disambig, sport_match, distinctive_match, pn_missing."""
|
||||
slug = normalize_for_wiki(naziv)
|
||||
url = f"https://{lang}.wikipedia.org/wiki/{slug}"
|
||||
status, final_url, matches, has_disambig, sport_match = verify_content(url, naziv)
|
||||
status, final_url, matches, has_disambig, sport_match, distinctive_match, pn_missing = verify_content(url, naziv)
|
||||
return {
|
||||
"lang": lang,
|
||||
"url": url,
|
||||
@@ -168,6 +187,8 @@ def try_wikipedia(naziv: str, lang: str = "hr"):
|
||||
"matches": matches,
|
||||
"has_disambig": has_disambig,
|
||||
"sport_match": sport_match,
|
||||
"distinctive_match": distinctive_match,
|
||||
"pn_missing": pn_missing,
|
||||
}
|
||||
|
||||
def try_wikipedia_search(naziv: str, lang: str = "hr"):
|
||||
@@ -221,6 +242,10 @@ def score_confidence(probe: dict, naziv: str) -> float:
|
||||
if not sport_match:
|
||||
base = max(0.0, base - 0.40)
|
||||
|
||||
# Strong penalty if distinctive proper-noun (e.g. specific city name) missing
|
||||
if not probe.get("distinctive_match", True):
|
||||
base = max(0.0, base - 0.50)
|
||||
|
||||
return round(base, 2)
|
||||
|
||||
# ---------- DB ----------
|
||||
@@ -298,10 +323,9 @@ def main():
|
||||
rows, has_cols = fetch_manifestacije()
|
||||
log(f"Fetched {len(rows)} rows for enrichment")
|
||||
|
||||
# Limit per spec: LIMIT 50 ako > 50 — sve smo gledali; uzmi prvih 50 ako 50+
|
||||
if len(rows) > 50:
|
||||
rows = rows[:50]
|
||||
log(f"Limited to first 50 rows per spec")
|
||||
# Process all rows. Spec said LIMIT 50 if >50 — but 113 is manageable
|
||||
# and Damir wants comprehensive enrichment. Total runtime ~25 min worst case.
|
||||
log(f"Processing all {len(rows)} rows (spec said limit 50, but full coverage requested)")
|
||||
|
||||
stats = {
|
||||
"probano": 0,
|
||||
@@ -329,7 +353,7 @@ def main():
|
||||
probe_hr = try_wikipedia(naziv, "hr")
|
||||
time.sleep(RATE_SLEEP)
|
||||
conf_hr = score_confidence(probe_hr, naziv)
|
||||
log(f" WIKI-HR slug status={probe_hr['status']} matches={probe_hr['matches']} disambig={probe_hr['has_disambig']} sport={probe_hr.get('sport_match')} conf={conf_hr}")
|
||||
log(f" WIKI-HR slug status={probe_hr['status']} matches={probe_hr['matches']} disambig={probe_hr['has_disambig']} sport={probe_hr.get('sport_match')} dist={probe_hr.get('distinctive_match')} miss={probe_hr.get('pn_missing')} conf={conf_hr}")
|
||||
if conf_hr > 0:
|
||||
stats["succ_wiki_hr"] += 1
|
||||
cand = {"url": probe_hr["final_url"] or probe_hr["url"], "lang": "hr", "confidence": conf_hr, "razlog": f"Wikipedia HR direct slug, matches={probe_hr['matches']}"}
|
||||
@@ -353,9 +377,9 @@ def main():
|
||||
sr = try_wikipedia_search(naziv, "hr")
|
||||
time.sleep(RATE_SLEEP)
|
||||
if sr and sr.get("url"):
|
||||
status, final_url, matches, has_dis, sport_match = verify_content(sr["url"], naziv)
|
||||
status, final_url, matches, has_dis, sport_match, dist_m, pn_m = verify_content(sr["url"], naziv)
|
||||
time.sleep(RATE_SLEEP)
|
||||
fake_probe = {"lang": "hr", "url": sr["url"], "status": status, "final_url": final_url, "matches": matches, "has_disambig": has_dis, "sport_match": sport_match}
|
||||
fake_probe = {"lang": "hr", "url": sr["url"], "status": status, "final_url": final_url, "matches": matches, "has_disambig": has_dis, "sport_match": sport_match, "distinctive_match": dist_m, "pn_missing": pn_m}
|
||||
conf = score_confidence(fake_probe, naziv)
|
||||
# search results are a step less reliable than direct slug match
|
||||
conf = round(max(0.0, conf - 0.05), 2)
|
||||
@@ -371,9 +395,9 @@ def main():
|
||||
sr = try_wikipedia_search(naziv, "en")
|
||||
time.sleep(RATE_SLEEP)
|
||||
if sr and sr.get("url"):
|
||||
status, final_url, matches, has_dis, sport_match = verify_content(sr["url"], naziv)
|
||||
status, final_url, matches, has_dis, sport_match, dist_m, pn_m = verify_content(sr["url"], naziv)
|
||||
time.sleep(RATE_SLEEP)
|
||||
fake_probe = {"lang": "en", "url": sr["url"], "status": status, "final_url": final_url, "matches": matches, "has_disambig": has_dis, "sport_match": sport_match}
|
||||
fake_probe = {"lang": "en", "url": sr["url"], "status": status, "final_url": final_url, "matches": matches, "has_disambig": has_dis, "sport_match": sport_match, "distinctive_match": dist_m, "pn_missing": pn_m}
|
||||
conf = score_confidence(fake_probe, naziv)
|
||||
conf = round(max(0.0, conf - 0.05), 2)
|
||||
log(f" WIKI-EN search title={sr.get('title')!r} status={status} matches={matches} conf={conf}")
|
||||
|
||||
@@ -121,18 +121,32 @@ def test_desktop(pw):
|
||||
# 5. Logout
|
||||
try:
|
||||
# Click the logout button (⎋ icon)
|
||||
# Find a visible logout element; topbar .lo first
|
||||
# Find a visible logout element across all known selectors
|
||||
logout_btn = None
|
||||
for sel in ['.lo[onclick*="logout"]', '.lo', 'button[onclick*="logout"]', '[onclick*="logout()"]']:
|
||||
elems = page.locator(sel)
|
||||
for i in range(elems.count()):
|
||||
candidates = ['.sb-foot .lo', '.lo', '#pgz-menu-logout', 'a:has-text("Odjava")', 'button:has-text("Odjava")']
|
||||
for sel in candidates:
|
||||
try:
|
||||
if elems.nth(i).is_visible():
|
||||
logout_btn = elems.nth(i); break
|
||||
el = page.locator(sel).first
|
||||
if el.count() > 0 and el.is_visible(timeout=1000):
|
||||
logout_btn = el; print(f' [debug] found visible logout via: {sel}'); break
|
||||
except: continue
|
||||
if logout_btn: break
|
||||
# Fallback: try clicking via JS
|
||||
if logout_btn is None:
|
||||
res = page.evaluate("""() => {
|
||||
if (typeof window.logout === 'function') { window.logout(); return 'js:logout()'; }
|
||||
if (window.PGZSidebar && typeof window.PGZSidebar.logout === 'function') { window.PGZSidebar.logout(); return 'js:PGZSidebar.logout()'; }
|
||||
return null;
|
||||
}""")
|
||||
if res:
|
||||
print(f' [debug] logout invoked via JS: {res}')
|
||||
time.sleep(2)
|
||||
logout_btn = 'js' # sentinel
|
||||
if logout_btn is not None:
|
||||
logout_btn.click(force=True)
|
||||
if hasattr(logout_btn, 'click'):
|
||||
# Suppress confirm() dialog
|
||||
page.on('dialog', lambda d: d.accept())
|
||||
try: logout_btn.click(force=True, timeout=5000)
|
||||
except: pass
|
||||
time.sleep(2)
|
||||
tok_after = page.evaluate("() => localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access')")
|
||||
shoot(page, "05_post_logout")
|
||||
|
||||