Round 2 mobile pass: CTA pinned above bottom sheet, swipe hint in chart header, anchor scroll-margin
- 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.
This commit is contained in:
1 parent
53315e03e8
commit
1b194b6f92
37 files changed
+1007
-21
No files matched your search
@@ -0,0 +1,98 @@
|
||||
"""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')
|
||||
Reference in new issue
Block a user