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:
1 parent
73132a1edf
commit
53315e03e8
37 files changed
+625
-21
No files matched your search
@@ -0,0 +1,126 @@
|
||||
"""Mobile UX audit for https://retire.krisforbes.ca — screenshots at phone viewports.
|
||||
Sets localStorage answers, reloads, captures key screens. Output: scratch/mobile-audit/*.png
|
||||
"""
|
||||
import json, os, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
OUT = "/home/ubuntu/projects/retire-planner/audit/mobile-audit-run"
|
||||
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'
|
||||
|
||||
DEFAULTS = {
|
||||
"province": None, "age": 45, "household": "single",
|
||||
"partner": {"age": 45, "income": 0, "rrsp": 0, "tfsa": 0, "nonreg": 0, "monthlyContribution": 0},
|
||||
"income": 60000, "rrsp": 100000, "tfsa": 30000, "nonreg": 20000,
|
||||
"monthlyContribution": 500, "employerMatchMonthly": 0,
|
||||
"retireAge": 65, "spendingAnnual": 50000,
|
||||
"cppStartAge": 65, "oasStartAge": 65,
|
||||
"pensionAnnual": 0, "rentalAnnual": 0, "otherAnnual": 0,
|
||||
"realReturnPre": 3.5, "realReturnPost": 2.5, "inflation": 2.0, "lifeExp": 95,
|
||||
"interests": []
|
||||
}
|
||||
ANSWERS = dict(DEFAULTS)
|
||||
ANSWERS.update({
|
||||
"province": "ON", "age": 45, "income": 85000, "rrsp": 250000, "tfsa": 60000,
|
||||
"nonreg": 40000, "monthlyContribution": 800, "retireAge": 65, "spendingAnnual": 60000,
|
||||
"pensionAnnual": 12000, "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'),
|
||||
]
|
||||
|
||||
# (device, tag, screen, step, extra_action)
|
||||
SHOTS = [
|
||||
('iphone14', 'intro', 'intro', 0, None),
|
||||
('iphone14', 'quiz-province','quiz', 0, None),
|
||||
('iphone14', 'quiz-income', 'quiz', 3, None),
|
||||
('iphone14', 'quiz-slider', 'quiz', 7, None),
|
||||
('iphone14', 'quiz-sheet-open','quiz', 3, 'open_sheet'),
|
||||
('iphone14', 'results-top', 'results', 0, None),
|
||||
('iphone14', 'results-charts','results', 0, 'charts'),
|
||||
('iphone14', 'results-table','results', 0, 'table'),
|
||||
('pixel', 'quiz-province','quiz', 0, None),
|
||||
('pixel', 'results-charts','results', 0, 'charts'),
|
||||
('se', 'quiz-income', 'quiz', 3, None),
|
||||
('se', 'results-charts','results', 0, 'charts'),
|
||||
]
|
||||
|
||||
def set_state(page, screen, step):
|
||||
page.evaluate("""([ls, a, screen, step]) => {
|
||||
const st = {a: a, step: step, screen: screen};
|
||||
localStorage.setItem(ls, JSON.stringify(st));
|
||||
}""", [LS, ANSWERS, screen, step])
|
||||
|
||||
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')
|
||||
for d, tag, screen, step, action in [s for s in SHOTS if s[0] == dev]:
|
||||
set_state(page, screen, step)
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(700)
|
||||
if action == 'open_sheet':
|
||||
try:
|
||||
page.click('.panel-h', timeout=3000)
|
||||
page.wait_for_timeout(400)
|
||||
except Exception as e:
|
||||
print('sheet open failed:', e)
|
||||
elif action == 'charts':
|
||||
el = page.locator('#g1')
|
||||
if el.count():
|
||||
el.scroll_into_view_if_needed()
|
||||
page.wait_for_timeout(600) # let G4 async render
|
||||
elif action == 'table':
|
||||
el = page.locator('#yr-table')
|
||||
if el.count():
|
||||
el.scroll_into_view_if_needed()
|
||||
page.wait_for_timeout(300)
|
||||
page.wait_for_timeout(300)
|
||||
path = os.path.join(OUT, f'{dev}-{tag}.png')
|
||||
page.screenshot(path=path, full_page=(action is None))
|
||||
print('saved', path)
|
||||
# also measure key geometry on the widest phone for the report
|
||||
if dev == 'iphone14':
|
||||
set_state(page, 'quiz', 3)
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(500)
|
||||
geo = page.evaluate("""() => {
|
||||
const r = s => { const e = document.querySelector(s); if (!e) return null; const b = e.getBoundingClientRect(); return {w: Math.round(b.width), h: Math.round(b.height), top: Math.round(b.top + window.scrollY), bottom: Math.round(b.bottom + window.scrollY)}; };
|
||||
return {
|
||||
docH: document.documentElement.scrollHeight,
|
||||
innerH: window.innerHeight,
|
||||
panel: r('.panel'), panelH: r('.panel-h'), chips: r('#panel-chips'),
|
||||
stepBody: r('#step-body'), btnNext: r('#btn-next'), btnBack: r('#btn-back'),
|
||||
hdr: r('.hdr'), g1: r('#g1'), tblWrap: r('.tbl-wrap'),
|
||||
minTouch: Math.min(...[...document.querySelectorAll('.ghost,.opt,.cta')].map(e => Math.round(e.getBoundingClientRect().height)))
|
||||
};
|
||||
}""")
|
||||
print('GEO iphone14 quiz:', json.dumps(geo))
|
||||
set_state(page, 'results', 0)
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(1200)
|
||||
geo2 = page.evaluate("""() => {
|
||||
const r = s => { const e = document.querySelector(s); if (!e) return null; const b = e.getBoundingClientRect(); return {w: Math.round(b.width), h: Math.round(b.height)}; };
|
||||
const svg = document.querySelector('#g1 svg');
|
||||
const txt = svg ? svg.querySelector('text') : null;
|
||||
return {
|
||||
g1: r('#g1'), g1svg: svg ? {w: Math.round(svg.getBoundingClientRect().width), h: Math.round(svg.getBoundingClientRect().height)} : null,
|
||||
firstSvgTextPx: txt ? Math.round(640 / svg.getBoundingClientRect().width * 11) : null,
|
||||
tblScrollW: (() => { const t = document.querySelector('.tbl-wrap'); return t ? {clientW: t.clientWidth, scrollW: t.scrollWidth} : null; })(),
|
||||
visionImg: r('.vision img')
|
||||
};
|
||||
}""")
|
||||
print('GEO iphone14 results:', json.dumps(geo2))
|
||||
ctx.close()
|
||||
browser.close()
|
||||
print('AUDIT-DONE')
|
||||
Reference in new issue
Block a user