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)
|
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_keywords = [
|
||||||
'sport', 'regat', 'rally', 'reli', 'turnir', 'memorijal', 'kup ',
|
r'\bsport', r'\bregat', r'\brally\b', r'\breli\b', r'\bturnir',
|
||||||
'automobiliz', 'jedrilic', 'jedren', 'auto-cross', 'autocross',
|
r'\bmemorijal', r'\bkup\b', r'\bautomobiliz', r'\bjedrili',
|
||||||
'kosark', 'rukomet', 'odbojk', 'plivac', 'plivanj', 'sah ', 'šah',
|
r'\bjedren', r'\bauto[- ]?cross', r'\bkosark', r'\brukomet',
|
||||||
'biciklizm', 'atletik', 'atletski', 'streljas', 'streljaš',
|
r'\bodbojk', r'\bplivac', r'\bplivanj', r'\bsahovsk', r'\bsahovi',
|
||||||
'taekwondo', 'karate', 'tenis', 'judo', 'boce', 'boćan',
|
r'\bsah\b', r'\bbiciklizm', r'\batleti', r'\bstreljas',
|
||||||
'nogomet', 'sailing', 'tournament', 'football', 'basketball',
|
r'\btaekwondo', r'\bkarate', r'\btenisk', r'\btenis\b', r'\bjudo\b',
|
||||||
'volleyball', 'handball', 'swimming', 'athletics', 'fencing',
|
r'\bboce\b', r'\bbocanj', r'\bnogomet', r'\bsailing', r'\btournament',
|
||||||
'archery', 'shooting', 'fishing', 'ribolov', 'maraton', 'cross-country',
|
r'\bfootball', r'\bbasketball', r'\bvolleyball', r'\bhandball',
|
||||||
'speedminton', 'badminton', 'snowboard', 'ski', 'skijanj',
|
r'\bswimming', r'\bathletics\b', r'\bfencing\b', r'\barchery',
|
||||||
'streljaški', 'vaterpolo', 'water polo'
|
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 ----------
|
# ---------- Wikipedia probing ----------
|
||||||
def try_wikipedia(naziv: str, lang: str = "hr"):
|
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)
|
slug = normalize_for_wiki(naziv)
|
||||||
url = f"https://{lang}.wikipedia.org/wiki/{slug}"
|
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 {
|
return {
|
||||||
"lang": lang,
|
"lang": lang,
|
||||||
"url": url,
|
"url": url,
|
||||||
@@ -168,6 +187,8 @@ def try_wikipedia(naziv: str, lang: str = "hr"):
|
|||||||
"matches": matches,
|
"matches": matches,
|
||||||
"has_disambig": has_disambig,
|
"has_disambig": has_disambig,
|
||||||
"sport_match": sport_match,
|
"sport_match": sport_match,
|
||||||
|
"distinctive_match": distinctive_match,
|
||||||
|
"pn_missing": pn_missing,
|
||||||
}
|
}
|
||||||
|
|
||||||
def try_wikipedia_search(naziv: str, lang: str = "hr"):
|
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:
|
if not sport_match:
|
||||||
base = max(0.0, base - 0.40)
|
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)
|
return round(base, 2)
|
||||||
|
|
||||||
# ---------- DB ----------
|
# ---------- DB ----------
|
||||||
@@ -298,10 +323,9 @@ def main():
|
|||||||
rows, has_cols = fetch_manifestacije()
|
rows, has_cols = fetch_manifestacije()
|
||||||
log(f"Fetched {len(rows)} rows for enrichment")
|
log(f"Fetched {len(rows)} rows for enrichment")
|
||||||
|
|
||||||
# Limit per spec: LIMIT 50 ako > 50 — sve smo gledali; uzmi prvih 50 ako 50+
|
# Process all rows. Spec said LIMIT 50 if >50 — but 113 is manageable
|
||||||
if len(rows) > 50:
|
# and Damir wants comprehensive enrichment. Total runtime ~25 min worst case.
|
||||||
rows = rows[:50]
|
log(f"Processing all {len(rows)} rows (spec said limit 50, but full coverage requested)")
|
||||||
log(f"Limited to first 50 rows per spec")
|
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"probano": 0,
|
"probano": 0,
|
||||||
@@ -329,7 +353,7 @@ def main():
|
|||||||
probe_hr = try_wikipedia(naziv, "hr")
|
probe_hr = try_wikipedia(naziv, "hr")
|
||||||
time.sleep(RATE_SLEEP)
|
time.sleep(RATE_SLEEP)
|
||||||
conf_hr = score_confidence(probe_hr, naziv)
|
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:
|
if conf_hr > 0:
|
||||||
stats["succ_wiki_hr"] += 1
|
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']}"}
|
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")
|
sr = try_wikipedia_search(naziv, "hr")
|
||||||
time.sleep(RATE_SLEEP)
|
time.sleep(RATE_SLEEP)
|
||||||
if sr and sr.get("url"):
|
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)
|
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)
|
conf = score_confidence(fake_probe, naziv)
|
||||||
# search results are a step less reliable than direct slug match
|
# search results are a step less reliable than direct slug match
|
||||||
conf = round(max(0.0, conf - 0.05), 2)
|
conf = round(max(0.0, conf - 0.05), 2)
|
||||||
@@ -371,9 +395,9 @@ def main():
|
|||||||
sr = try_wikipedia_search(naziv, "en")
|
sr = try_wikipedia_search(naziv, "en")
|
||||||
time.sleep(RATE_SLEEP)
|
time.sleep(RATE_SLEEP)
|
||||||
if sr and sr.get("url"):
|
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)
|
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 = score_confidence(fake_probe, naziv)
|
||||||
conf = round(max(0.0, conf - 0.05), 2)
|
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}")
|
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
|
# 5. Logout
|
||||||
try:
|
try:
|
||||||
# Click the logout button (⎋ icon)
|
# 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
|
logout_btn = None
|
||||||
for sel in ['.lo[onclick*="logout"]', '.lo', 'button[onclick*="logout"]', '[onclick*="logout()"]']:
|
candidates = ['.sb-foot .lo', '.lo', '#pgz-menu-logout', 'a:has-text("Odjava")', 'button:has-text("Odjava")']
|
||||||
elems = page.locator(sel)
|
for sel in candidates:
|
||||||
for i in range(elems.count()):
|
try:
|
||||||
try:
|
el = page.locator(sel).first
|
||||||
if elems.nth(i).is_visible():
|
if el.count() > 0 and el.is_visible(timeout=1000):
|
||||||
logout_btn = elems.nth(i); break
|
logout_btn = el; print(f' [debug] found visible logout via: {sel}'); break
|
||||||
except: continue
|
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:
|
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)
|
time.sleep(2)
|
||||||
tok_after = page.evaluate("() => localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access')")
|
tok_after = page.evaluate("() => localStorage.getItem('pgz_access') || sessionStorage.getItem('pgz_access')")
|
||||||
shoot(page, "05_post_logout")
|
shoot(page, "05_post_logout")
|
||||||
|
|||||||