Mobile pass: 8 fixes per MOBILE-FIX.md (single-row 55px header, 560px-scroll charts w/ bigger SVG fonts + swipe hint, content-height chips sheet w/ scrim, 44px touch targets, year table 13px/-2 cols/fade/sticky Age, vision manifest-race fix, honest share fallback, theme-color+type bumps); before/after audits + geo-check evidence

This commit is contained in:
krisf committed 2026-10-09 06:04:03 -04:00
1 parent be57deb5a2
commit 666b6c5e29
37 files changed
+625 -21

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
"""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()