- Issue 1: --sheet-h CSS var (live sheet height via syncSheetH: load/toggle/resize/
panel-content), .scr clearance and sticky #btn-next/.step-nav above the sheet;
income short step pixel-identical to round 1 (CTA top 485).
- Issue 2: chart() helper renders .chart-h (h3+p + .swipe span) into #g1-#g4;
.chartbox::before overlay removed; hint is flex sibling in header, sticky left.
- Issue 3: [id]{scroll-margin-top:70px} so #whatif anchor lands below sticky hdr.
- G2 tick clip and Four-futures scroll documented as non-issues in report.
- verify2.py: 90/90 PASS live; before/after evidence in audit/mobile-{before,after}2/.
Spec: MOBILE-FIX-2.md. Report: MOBILE-FIX-2-REPORT.md.
128 lines
6.6 KiB
Python
128 lines
6.6 KiB
Python
"""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
|
|
|
|
import sys
|
|
OUT = sys.argv[1] if len(sys.argv) > 1 else '/home/ubuntu/.hermes/cache/scratch/mobile-audit'
|
|
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')
|