"""Spec MOBILE-FIX.md step 6: programmatic geometry re-check at 390px (after fixes).""" import json from playwright.sync_api import sync_playwright 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, "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"]} with sync_playwright() as pw: b = pw.chromium.launch(executable_path=CHROME, headless=True, args=['--no-sandbox','--disable-dev-shm-usage']) ctx = b.new_context(viewport={'width': 390, 'height': 844}, device_scale_factor=3.0, is_mobile=True, has_touch=True, user_agent='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') page = ctx.new_page() page.goto(BASE, wait_until='networkidle') def set_state(screen, step): page.evaluate("""([ls,a,screen,step]) => localStorage.setItem(ls, JSON.stringify({a,step,screen}))""", [LS, ANSWERS, screen, step]) R = {} # --- quiz screen --- set_state('quiz', 3) page.reload(wait_until='networkidle'); page.wait_for_timeout(700) R['quiz'] = page.evaluate("""() => { const vis = e => { const b=e.getBoundingClientRect(); return b.width>0 && b.height>0; }; const hdr = document.querySelector('.hdr').getBoundingClientRect(); const touches = [...document.querySelectorAll('.ghost,.opt,.cta')].filter(vis) .map(e => ({id: e.id||e.className, h: Math.round(e.getBoundingClientRect().height)})); const minTouch = Math.min(...touches.map(t=>t.h)); const panel = document.querySelector('.panel').getBoundingClientRect(); const chips = [...document.querySelectorAll('#panel-chips .chip')]; const chipVals = chips.map(c => { const v = c.querySelector('.v'), b = v.getBoundingClientRect(); const r = document.createRange(); r.selectNodeContents(v); const rb = r.getBoundingClientRect(); return {label: c.querySelector('.k').textContent, value: v.textContent, fullyInView: b.top>=0 && b.bottom<=window.innerHeight && rb.bottom<=window.innerHeight, h: Math.round(b.height)}; }); const sheetCollapsed = !document.querySelector('.panel').classList.contains('open'); const insightShown = getComputedStyle(document.querySelector('.insight')).display!=='none'; const next = document.querySelector('#btn-next').getBoundingClientRect(); const theme = document.querySelector('meta[name=theme-color]'); return {hdrH: Math.round(hdr.height), hdrRows: Math.round(hdr.height/40), touchTargets: touches, minTouch, panelTop: Math.round(panel.top), panelBottom: Math.round(panel.bottom), innerH: window.innerHeight, nChips: chips.length, chipVals, sheetCollapsed, insightShownByDefault: insightShown, nextClearsSheet: Math.round(next.bottom) <= Math.round(panel.top), themeColor: theme ? theme.content : null}; }""") # sheet expand + scrim page.click('.panel-h'); page.wait_for_timeout(400) R['sheet_open'] = page.evaluate("""() => { const p = document.querySelector('.panel'), s = document.getElementById('scrim'); return {open: p.classList.contains('open'), insightShown: getComputedStyle(document.querySelector('.insight')).display!=='none', scrimVisible: !!s && !s.hidden && getComputedStyle(s).display!=='none', scrimZ: +getComputedStyle(s).zIndex, panelZ: +getComputedStyle(p).zIndex, hdrZ: +getComputedStyle(document.querySelector('.hdr')).zIndex}; }""") R['scrim_close'] = page.evaluate("""() => { document.getElementById('scrim').click(); const p=document.querySelector('.panel'), s=document.getElementById('scrim'); return {panelOpen: p.classList.contains('open'), scrimHidden: s.hidden}; }""") # --- results screen --- set_state('results', 0) page.reload(wait_until='networkidle'); page.wait_for_timeout(1500) R['results'] = page.evaluate("""() => { const box = document.querySelector('#g1'); const svg = box.querySelector('svg'); const fs = svg ? +svg.querySelector('text').getAttribute('font-size') : null; const rendered = fs ? Math.round(fs * svg.getBoundingClientRect().width / 640 * 10)/10 : null; const tw = document.querySelector('.tbl-wrap'); const th = document.querySelector('#yr-table th:nth-child(4)'); const th9 = document.querySelector('#yr-table th:nth-child(9)'); const cs = getComputedStyle(document.querySelector('table')); return {g1svgW: Math.round(svg.getBoundingClientRect().width), g1svgMinW: getComputedStyle(svg).minWidth, g1scrollable: box.scrollWidth > box.clientWidth, g1clientW: box.clientWidth, g1scrollW: box.scrollWidth, tickFontSizeAttr: fs, tickRenderedPx: rendered, legendPx: getComputedStyle(document.querySelector('.legend')).fontSize, tblFont: cs.fontSize, col4Hidden: getComputedStyle(th).display==='none', col9Hidden: getComputedStyle(th9).display==='none', tblScrollW: tw.scrollWidth, tblClientW: tw.clientWidth, fadePresent: getComputedStyle(tw,'::before').backgroundImage.includes('gradient'), visionIsImg: !!document.querySelector('#vision-card img'), visionGrad: document.querySelector('#vision-card').classList.contains('grad')}; }""") # table scrolled right: Age column sticky? page.evaluate("() => { document.querySelector('details.yearly').open = true; }") page.wait_for_timeout(200) page.locator('#yr-table').scroll_into_view_if_needed(); page.wait_for_timeout(300) R['table_sticky'] = page.evaluate("""() => { const tw = document.querySelector('.tbl-wrap'); tw.scrollLeft = 300; const first = document.querySelector('#yr-table td:first-child').getBoundingClientRect(); const box = tw.getBoundingClientRect(); return {scrolled: tw.scrollLeft, ageColVisible: first.left >= box.left - 1 && first.right > box.left, ageColLeftInBox: Math.round(first.left - box.left)}; }""") # chart swipe hint visible? R['swipe_hint'] = page.evaluate("""() => { const b=document.querySelector('#g1'); const c=getComputedStyle(b,'::after').content + '|' + getComputedStyle(b,'::before').content; return {hasSwipe: c.includes('swipe')}; }""") print(json.dumps(R, indent=1)) ctx.close(); b.close()