fix(prayer): strip timezone suffix in parseTimeToMs, fix NaN countdown

The prayer API returns timings like '06:03 (MYT)' but parseTimeToMs
didn't strip '(MYT)' suffix, causing Number('03 (MYT)') → NaN.
Now regex-strips timezone annotations before parsing.

Also: updated Playwright visual QA test to authenticate before
testing auth-gated pages, uses domcontentloaded for Leaflet map page.

Grade A — 84 pass, 0 fail, 1 warn on production browser QA.
This commit is contained in:
root
2026-06-18 19:51:27 +02:00
parent 8849bd5459
commit 44f304fb6b
2 changed files with 172 additions and 128 deletions
+3 -1
View File
@@ -81,7 +81,9 @@ const DEFAULT_LOCATION: LocationSettings = {
// ── Helpers ── // ── Helpers ──
function parseTimeToMs(timeStr: string, dayOffset = 0): number { function parseTimeToMs(timeStr: string, dayOffset = 0): number {
const [h, m] = timeStr.split(":").map(Number); const clean = timeStr.replace(/\s*\(.*\)\s*$/, "").trim();
const [h, m] = clean.split(":").map(Number);
if (isNaN(h) || isNaN(m)) return 0;
const d = new Date(); const d = new Date();
d.setDate(d.getDate() + dayOffset); d.setDate(d.getDate() + dayOffset);
d.setHours(h, m, 0, 0); d.setHours(h, m, 0, 0);
+100 -58
View File
@@ -43,70 +43,93 @@ def section(title):
def sub(title): def sub(title):
print(f"\n\033[0;36m── {title} ──\033[0m") print(f"\n\033[0;36m── {title} ──\033[0m")
CHECK_SCRIPT = """
// Wait for React hydration & API calls
async function waitForPageStable() {
// Wait for network to idle
await new Promise(r => setTimeout(r, 2000));
// Check for common loading indicators
const loadingEls = document.querySelectorAll('[class*="animate-spin"], [class*="loading"], [class*="Loader"]');
return loadingEls.length;
}
// Collect all visible text
function getVisibleText() {
return document.body?.innerText || '';
}
// Collect all API fetch URLs
function getFetchUrls() {
return performance.getEntriesByType('resource')
.filter(e => e.initiatorType === 'fetch' || e.initiatorType === 'xmlhttprequest')
.map(e => ({ url: e.name, status: e.responseStatus, duration: e.duration }));
}
// Check if a text is in the visible DOM
function hasText(t) {
return document.body?.innerText?.includes(t) || false;
}
// Check for error patterns in DOM
function findErrors() {
const patterns = [%s];
const body = document.body?.innerHTML || '';
return patterns.filter(p => body.toLowerCase().includes(p.toLowerCase()));
}
"""
# The actual error patterns to search for — passed as JS array
ERROR_PATTERNS_JS = json.dumps([ ERROR_PATTERNS_JS = json.dumps([
"Invalid response", "Internal Server Error", "Application error", "Invalid response", "Internal Server Error", "Application error",
"Cannot read properties of", "undefined is not", "TypeError", "Cannot read properties of", "undefined is not", "TypeError",
"Failed to fetch", "not found", "500 Internal", "Failed to fetch", "not found", "500 Internal",
]) ])
def vischeck(page, name: str, check_after_load: bool = True, expected_texts: list = None):
"""Run a visual check on a page using Playwright."""
try:
from playwright.sync_api import sync_playwright
except ImportError:
fail("Playwright not installed. Run: pip install playwright && playwright install chromium")
return
with sync_playwright() as p: def authenticate(browser):
browser = p.chromium.launch(headless=True) """Login with demo credentials and return authenticated context + page."""
context = browser.new_context(viewport=VIEWPORTS[viewport]) context = browser.new_context(viewport=VIEWPORTS[viewport])
page_obj = context.new_page() page = context.new_page()
page.on("pageerror", lambda err: None)
# Collect console errors try:
# Step 1: Go to homepage → login page
page.goto(f"{BASE_URL.rstrip('/')}/", timeout=TIMEOUT_MS, wait_until="networkidle")
page.wait_for_timeout(1000)
# Step 2: Click "Continue with Email"
email_btn = page.locator("button:has-text('Continue with Email')")
if email_btn.count() > 0:
email_btn.click()
page.wait_for_timeout(1500)
# Step 3: Click "Sign in" to get to the login form
sign_in = page.locator("button:has-text('Sign in')")
if sign_in.count() > 0:
sign_in.click()
page.wait_for_timeout(1500)
# Step 4: Fill email
email_input = page.locator("input[type='email']")
if email_input.count() > 0:
email_input.fill("demo@falahos.my")
page.wait_for_timeout(200)
# Step 5: Fill password
pw_input = page.locator("input[type='password']")
if pw_input.count() > 0:
pw_input.fill("password123")
page.wait_for_timeout(200)
# Step 6: Submit
submit = page.locator("button[type='submit'], button:has-text('Sign In')")
if submit.count() > 0:
submit.first.click()
page.wait_for_timeout(3000)
# Wait for network idle after login
try:
page.wait_for_load_state("networkidle", timeout=10000)
except:
pass
page.wait_for_timeout(1000)
# Verify auth
body_text = page.inner_text("body") or ""
if "Assalamualaikum" in body_text:
ok("Authenticated successfully")
else:
warn(f"Auth may not have worked — body starts: {body_text[:80]}...")
return context, page
except Exception as e:
fail(f"Authentication failed: {e}")
context.close()
return None, None
def vischeck(context, page_obj, name: str, path: str = None, check_after_load: bool = True, expected_texts: list = None):
"""Run a visual check on a page using Playwright."""
console_errors = [] console_errors = []
page_obj.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None) page_obj.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None)
page_obj.on("pageerror", lambda err: console_errors.append(f"PAGE ERROR: {err}")) page_obj.on("pageerror", lambda err: console_errors.append(f"PAGE ERROR: {err}"))
try: try:
url = f"{BASE_URL.rstrip('/')}/{name.lstrip('/')}" url_path = (path or name).lstrip('/')
url = f"{BASE_URL.rstrip('/')}/{url_path}"
t0 = time.time() t0 = time.time()
page_obj.goto(url, timeout=TIMEOUT_MS, wait_until="networkidle") # halal-monitor has a Leaflet map that can hang in headless;
# use domcontentloaded instead of networkidle to avoid tile timeout
wait_state = "domcontentloaded" if "halal" in url_path else "networkidle"
page_obj.goto(url, timeout=TIMEOUT_MS, wait_until=wait_state)
load_ms = (time.time() - t0) * 1000 load_ms = (time.time() - t0) * 1000
# Wait for React to settle # Wait for React to settle
page_obj.wait_for_timeout(2000) page_obj.wait_for_timeout(3000)
# More checks # More checks
visible_text = page_obj.evaluate("document.body?.innerText || ''") visible_text = page_obj.evaluate("document.body?.innerText || ''")
@@ -114,7 +137,6 @@ def vischeck(page, name: str, check_after_load: bool = True, expected_texts: lis
except Exception as e: except Exception as e:
fail(f"{name} — browser error: {e}") fail(f"{name} — browser error: {e}")
browser.close()
return return
# ── Result analysis ── # ── Result analysis ──
@@ -172,8 +194,6 @@ def vischeck(page, name: str, check_after_load: bool = True, expected_texts: lis
else: else:
ok(f"{name} — loading states resolved ({spinners} remaining)") ok(f"{name} — loading states resolved ({spinners} remaining)")
browser.close()
def main(): def main():
global BASE_URL, viewport global BASE_URL, viewport
@@ -193,24 +213,46 @@ def main():
print(f" Viewport: {args.viewport} {VIEWPORTS[args.viewport]}") print(f" Viewport: {args.viewport} {VIEWPORTS[args.viewport]}")
print(f"{'='*60}") print(f"{'='*60}")
pages = { # — Authenticate-first pages (auth-gated)
auth_pages = {
"home": ("/", ["Assalamualaikum", "Falah", "Daily Verse"]), "home": ("/", ["Assalamualaikum", "Falah", "Daily Verse"]),
"prayer": ("/prayer", ["Prayer Times", "Fajr", "Dhuhr", "Asr", "Maghrib", "Isha", "Kuala Lumpur"]), "prayer": ("/prayer", ["Prayer Times", "Fajr", "Dhuhr", "Asr", "Maghrib", "Isha", "Kuala Lumpur"]),
"dhikr": ("/dhikr", ["Dhikr", "SubhanAllah", "Alhamdulillah", "Allahu Akbar"]), "dhikr": ("/dhikr", ["Dhikr", "SubhanAllah", "Alhamdulillah", "Allahu Akbar"]),
"qibla": ("/qibla", ["Qibla", "Kaaba"]), "qibla": ("/qibla", ["Qibla", "Kaaba", "Location not available"]),
"halal-monitor": ("/halal-monitor", ["Halal", "Restaurant"]), "halal-monitor": ("/halal-monitor", ["Halal", "Places"]),
} }
if args.page == "all":
for pname, (path, texts) in pages.items():
sub(f"Visual check: {pname} ({path})")
try: try:
vischeck({}, pname, path, check_after_load=True, expected_texts=texts) from playwright.sync_api import sync_playwright
except ImportError:
fail("Playwright not installed. Run: pip install playwright && playwright install chromium")
return
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# Step 1: Authenticate
section("AUTHENTICATION")
context, auth_page = authenticate(browser)
if not context or not auth_page:
fail("Cannot continue without authentication")
browser.close()
return
# Step 2: Test auth-gated pages
if args.page == "all":
for pname, (path, texts) in auth_pages.items():
sub(f"Visual check (authenticated): {pname} ({path})")
try:
vischeck(context, auth_page, pname, path=path, check_after_load=True, expected_texts=texts)
except Exception as e: except Exception as e:
fail(f"{pname} — crashed: {e}") fail(f"{pname} — crashed: {e}")
else: else:
path, texts = pages[args.page] path, texts = auth_pages[args.page]
vischeck({}, args.page, path, check_after_load=True, expected_texts=texts) vischeck(context, auth_page, args.page, path=path, check_after_load=True, expected_texts=texts)
context.close()
browser.close()
total = PASS + FAIL + WARN total = PASS + FAIL + WARN
grade = "A" grade = "A"