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:
@@ -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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user