diff --git a/_audit/playwright_20260505_0922/01_login_page.png b/_audit/playwright_20260505_0922/01_login_page.png new file mode 100644 index 0000000..f1cfde1 Binary files /dev/null and b/_audit/playwright_20260505_0922/01_login_page.png differ diff --git a/_audit/playwright_20260505_0922/02_post_login.png b/_audit/playwright_20260505_0922/02_post_login.png new file mode 100644 index 0000000..c8ef008 Binary files /dev/null and b/_audit/playwright_20260505_0922/02_post_login.png differ diff --git a/_audit/playwright_20260505_0922/03_app_dashboard.png b/_audit/playwright_20260505_0922/03_app_dashboard.png new file mode 100644 index 0000000..c8ef008 Binary files /dev/null and b/_audit/playwright_20260505_0922/03_app_dashboard.png differ diff --git a/_audit/playwright_20260505_0922/04_profile_view.png b/_audit/playwright_20260505_0922/04_profile_view.png new file mode 100644 index 0000000..6ff2dfe Binary files /dev/null and b/_audit/playwright_20260505_0922/04_profile_view.png differ diff --git a/_audit/playwright_20260505_0922/05_post_logout.png b/_audit/playwright_20260505_0922/05_post_logout.png new file mode 100644 index 0000000..c8ef008 Binary files /dev/null and b/_audit/playwright_20260505_0922/05_post_logout.png differ diff --git a/_audit/playwright_20260505_0922/m01_mobile_login.png b/_audit/playwright_20260505_0922/m01_mobile_login.png new file mode 100644 index 0000000..82f0346 Binary files /dev/null and b/_audit/playwright_20260505_0922/m01_mobile_login.png differ diff --git a/_audit/playwright_20260505_0922/m02_mobile_app.png b/_audit/playwright_20260505_0922/m02_mobile_app.png new file mode 100644 index 0000000..6d5fc50 Binary files /dev/null and b/_audit/playwright_20260505_0922/m02_mobile_app.png differ diff --git a/_audit/playwright_20260505_0922/m03_mobile_sidebar_open.png b/_audit/playwright_20260505_0922/m03_mobile_sidebar_open.png new file mode 100644 index 0000000..3478c27 Binary files /dev/null and b/_audit/playwright_20260505_0922/m03_mobile_sidebar_open.png differ diff --git a/_audit/playwright_20260505_0922/m04_mobile_sport2_homepage.png b/_audit/playwright_20260505_0922/m04_mobile_sport2_homepage.png new file mode 100644 index 0000000..85ed733 Binary files /dev/null and b/_audit/playwright_20260505_0922/m04_mobile_sport2_homepage.png differ diff --git a/_audit/playwright_20260505_0922/results.json b/_audit/playwright_20260505_0922/results.json new file mode 100644 index 0000000..2eb39b0 --- /dev/null +++ b/_audit/playwright_20260505_0922/results.json @@ -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 + } +} \ No newline at end of file diff --git a/_audit/sub4_enrich.py b/_audit/sub4_enrich.py index 8435802..1ba39f3 100644 --- a/_audit/sub4_enrich.py +++ b/_audit/sub4_enrich.py @@ -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}") diff --git a/scripts/playwright_e2e.py b/scripts/playwright_e2e.py index cae71bb..68fc906 100755 --- a/scripts/playwright_e2e.py +++ b/scripts/playwright_e2e.py @@ -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()): - try: - if elems.nth(i).is_visible(): - logout_btn = elems.nth(i); break - except: continue - if logout_btn: break + candidates = ['.sb-foot .lo', '.lo', '#pgz-menu-logout', 'a:has-text("Odjava")', 'button:has-text("Odjava")'] + for sel in candidates: + try: + 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 + # 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")