"""Round-2 mobile measurements: step-0 CTA vs sheet, g2 tick clip, anchor scroll, at 390/360/320.""" import json, os from playwright.sync_api import sync_playwright import sys OUT = sys.argv[1] if len(sys.argv) > 1 else '/home/ubuntu/.hermes/cache/scratch/mobile-audit2' os.makedirs(OUT, exist_ok=True) CHROME = '/home/ubuntu/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome' BASE = 'https://retire.krisforbes.ca/' LS = 'retire-planner-v1' ANSWERS = { "province": "ON", "age": 45, "household": "single", "partner": {"age": 45, "income": 0, "rrsp": 0, "tfsa": 0, "nonreg": 0, "monthlyContribution": 0}, "income": 85000, "rrsp": 250000, "tfsa": 60000, "nonreg": 40000, "monthlyContribution": 800, "employerMatchMonthly": 0, "retireAge": 65, "spendingAnnual": 60000, "cppStartAge": 65, "oasStartAge": 65, "pensionAnnual": 12000, "rentalAnnual": 0, "otherAnnual": 0, "realReturnPre": 3.5, "realReturnPost": 2.5, "inflation": 2.0, "lifeExp": 95, "interests": ["dock", "travel"] } DEVICES = [ ('iphone14', 390, 844, 3.0, 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1'), ('pixel', 360, 800, 2.75, 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36'), ('se', 320, 700, 2.0, 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1'), ] with sync_playwright() as pw: browser = pw.chromium.launch(executable_path=CHROME, headless=True, args=['--no-sandbox', '--disable-dev-shm-usage']) for dev, w, h, dsf, ua in DEVICES: ctx = browser.new_context(viewport={'width': w, 'height': h}, device_scale_factor=dsf, is_mobile=True, has_touch=True, user_agent=ua) page = ctx.new_page() page.goto(BASE, wait_until='networkidle') # --- step 0 (province grid, tallest step) at initial scroll --- page.evaluate("""([ls, a]) => localStorage.setItem(ls, JSON.stringify({a: a, step: 0, screen: 'quiz'}))""", [LS, ANSWERS]) page.reload(wait_until='networkidle'); page.wait_for_timeout(600) page.evaluate("window.scrollTo(0,0)") g = page.evaluate("""() => { const rc = e => { const b = e.getBoundingClientRect(); return {top: Math.round(b.top), bottom: Math.round(b.bottom), h: Math.round(b.height)}; }; const panel = document.querySelector('.panel'); const next = document.querySelector('#btn-next'); const opts = [...document.querySelectorAll('.opt')]; const last = opts[opts.length-1]; const nc = next.getBoundingClientRect(); const elAt = document.elementFromPoint(nc.left + nc.width/2, nc.top + nc.height/2); return { innerH: window.innerHeight, docH: document.documentElement.scrollHeight, maxScroll: document.documentElement.scrollHeight - window.innerHeight, panelTop: Math.round(panel.getBoundingClientRect().top), sheetH: Math.round(panel.getBoundingClientRect().height), next: rc(next), nextBehindSheet: Math.round(nc.bottom) > Math.round(panel.getBoundingClientRect().top), nextTappable: !!(elAt && elAt.closest && elAt.closest('#btn-next')), lastOptText: last.textContent.trim().slice(0, 20), lastOpt: rc(last), lastOptBehindSheet: Math.round(last.getBoundingClientRect().bottom) > Math.round(panel.getBoundingClientRect().top) }; }""") print(f'{dev} STEP0:', json.dumps(g)) page.screenshot(path=f'{OUT}/{dev}-step0-viewport.png') # --- results: g2 bottom tick clip check + anchor scroll check --- page.evaluate("""([ls, a]) => localStorage.setItem(ls, JSON.stringify({a: a, step: 0, screen: 'results'}))""", [LS, ANSWERS]) page.reload(wait_until='networkidle'); page.wait_for_timeout(1200) g2 = page.evaluate("""() => { const box = document.querySelector('#g2'); const svg = box ? box.querySelector('svg') : null; if (!box || !svg) return {missing: true}; const bb = box.getBoundingClientRect(), sb = svg.getBoundingClientRect(); const ticks = [...svg.querySelectorAll('text')].map(t => t.textContent); return { boxH: Math.round(bb.height), svgH: Math.round(sb.height), svgFitsVertically: sb.height <= bb.height + 1, svgBottomMinusBoxBottom: Math.round(sb.bottom - bb.bottom), bottomTick: ticks.slice(-4) }; }""") print(f'{dev} G2:', json.dumps(g2)) page.evaluate("document.querySelector('#g2').scrollIntoView({block:'center'})") page.wait_for_timeout(300) page.screenshot(path=f'{OUT}/{dev}-g2-viewport.png') # --- anchor scroll under sticky header --- ha = page.evaluate("""() => { location.hash = '#whatif'; return new Promise(res => setTimeout(() => { const t = document.querySelector('#whatif'); const hdr = document.querySelector('.hdr'); if (!t) return res({missing: true}); const tr = t.getBoundingClientRect(), hr = hdr.getBoundingClientRect(); res({targetTop: Math.round(tr.top), hdrBottom: Math.round(hr.bottom), covered: tr.top < hr.bottom}); }, 500)); }""") print(f'{dev} HASH-ANCHOR:', json.dumps(ha)) ctx.close() browser.close() print('MEASURE2-DONE')