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:
krisf committed 2026-10-09 06:44:08 -04:00
1 parent 666b6c5e29
commit 65863c0302
37 files changed
+1007 -21

No files matched your search

+98
View File
@@ -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')
+15
View File
@@ -0,0 +1,15 @@
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/iphone14-intro.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/iphone14-quiz-province.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/iphone14-quiz-income.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/iphone14-quiz-slider.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/iphone14-quiz-sheet-open.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/iphone14-results-top.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/iphone14-results-charts.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/iphone14-results-table.png
GEO iphone14 quiz: {"docH": 844, "innerH": 844, "panel": {"w": 374, "h": 218, "top": 618, "bottom": 836}, "panelH": {"w": 344, "h": 22, "top": 633, "bottom": 655}, "chips": {"w": 344, "h": 158, "top": 663, "bottom": 821}, "stepBody": {"w": 358, "h": 320, "top": 147, "bottom": 467}, "btnNext": {"w": 358, "h": 50, "top": 485, "bottom": 535}, "btnBack": {"w": 75, "h": 44, "top": 103, "bottom": 147}, "hdr": {"w": 390, "h": 55, "top": 0, "bottom": 55}, "g1": {"w": 0, "h": 0, "top": 0, "bottom": 0}, "tblWrap": {"w": 0, "h": 0, "top": 0, "bottom": 0}, "minTouch": 0}
GEO iphone14 results: {"g1": {"w": 358, "h": 409}, "g1svg": {"w": 560, "h": 236}, "firstSvgTextPx": 13, "tblScrollW": {"clientW": 326, "scrollW": 508}, "visionImg": {"w": 356, "h": 250}}
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/pixel-quiz-province.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/pixel-results-charts.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/se-quiz-income.png
saved /home/ubuntu/projects/retire-planner/audit/mobile-after2/shots/se-results-charts.png
AUDIT-DONE
Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+10
View File
@@ -0,0 +1,10 @@
iphone14 STEP0: {"innerH": 844, "docH": 961, "maxScroll": 117, "panelTop": 618, "sheetH": 218, "next": {"top": 564, "bottom": 614, "h": 50}, "nextBehindSheet": false, "nextTappable": true, "lastOptText": "Nunavut", "lastOpt": {"top": 587, "bottom": 630, "h": 43}, "lastOptBehindSheet": true}
iphone14 G2: {"boxH": 437, "svgH": 236, "svgFitsVertically": true, "svgBottomMinusBoxBottom": -35, "bottomTick": ["80", "85", "90", "age"]}
iphone14 HASH-ANCHOR: {"targetTop": 1537, "hdrBottom": 55, "covered": false}
pixel STEP0: {"innerH": 800, "docH": 961, "maxScroll": 161, "panelTop": 574, "sheetH": 218, "next": {"top": 520, "bottom": 570, "h": 50}, "nextBehindSheet": false, "nextTappable": true, "lastOptText": "Nunavut", "lastOpt": {"top": 587, "bottom": 630, "h": 43}, "lastOptBehindSheet": true}
pixel G2: {"boxH": 437, "svgH": 236, "svgFitsVertically": true, "svgBottomMinusBoxBottom": -35, "bottomTick": ["80", "85", "90", "age"]}
pixel HASH-ANCHOR: {"targetTop": 1547, "hdrBottom": 55, "covered": false}
se STEP0: {"innerH": 700, "docH": 995, "maxScroll": 295, "panelTop": 474, "sheetH": 218, "next": {"top": 420, "bottom": 470, "h": 50}, "nextBehindSheet": false, "nextTappable": true, "lastOptText": "Nunavut", "lastOpt": {"top": 621, "bottom": 664, "h": 43}, "lastOptBehindSheet": true}
se G2: {"boxH": 469, "svgH": 236, "svgFitsVertically": true, "svgBottomMinusBoxBottom": -67, "bottomTick": ["80", "85", "90", "age"]}
se HASH-ANCHOR: {"targetTop": 1562, "hdrBottom": 55, "covered": false}
MEASURE2-DONE
Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

+121
View File
@@ -0,0 +1,121 @@
iphone14 step0 @scrollY=0: {"scrollY": 0, "maxScroll": 117, "panelTop": 618, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 97, "bottom": 153}, "next": {"top": 564, "bottom": 614}, "nextTappable": true, "lastOptBottom": 630}
PASS iphone14 step0: next bottom <= panelTop-2 @0 {"nextBottom": 614, "panelTop": 618}
PASS iphone14 step0: next tappable @0
iphone14 step0 @mid: {"scrollY": 59, "maxScroll": 117, "panelTop": 618, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 38, "bottom": 94}, "next": {"top": 564, "bottom": 614}, "nextTappable": true, "lastOptBottom": 571}
PASS iphone14 step0: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 94, "panelTop": 618}
PASS iphone14 step0: next still pinned mid (nextBottom <= panelTop-2) {"nextBottom": 614, "panelTop": 618}
iphone14 step0 @max: {"scrollY": 117, "maxScroll": 117, "panelTop": 618, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": -20, "bottom": 36}, "next": {"top": 550, "bottom": 600}, "nextTappable": true, "lastOptBottom": 513}
PASS iphone14 step0: last opt bottom <= panelTop-2 @max {"lastOptBottom": 513, "panelTop": 618}
PASS iphone14 step0: next tappable @max
iphone14 step13-interests @scrollY=0: {"scrollY": 0, "maxScroll": 6, "panelTop": 618, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 97, "bottom": 153}, "next": {"top": 556, "bottom": 606}, "nextTappable": true, "lastOptBottom": 519}
PASS iphone14 step13-interests: next bottom <= panelTop-2 @0 {"nextBottom": 606, "panelTop": 618}
PASS iphone14 step13-interests: next tappable @0
iphone14 step13-interests @mid: {"scrollY": 3, "maxScroll": 6, "panelTop": 618, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 94, "bottom": 150}, "next": {"top": 553, "bottom": 603}, "nextTappable": true, "lastOptBottom": 516}
PASS iphone14 step13-interests: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 150, "panelTop": 618}
PASS iphone14 step13-interests: next still pinned mid (nextBottom <= panelTop-2) {"nextBottom": 603, "panelTop": 618}
iphone14 step13-interests @max: {"scrollY": 6, "maxScroll": 6, "panelTop": 618, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 91, "bottom": 147}, "next": {"top": 550, "bottom": 600}, "nextTappable": true, "lastOptBottom": 513}
PASS iphone14 step13-interests: last opt bottom <= panelTop-2 @max {"lastOptBottom": 513, "panelTop": 618}
PASS iphone14 step13-interests: next tappable @max
iphone14 income-step3 (doc coords): {"navTop": 97, "navBottom": 153, "navPos": "sticky", "nextTop": 485, "nextBottom": 535, "panelTop": 618, "maxScroll": 0, "scrollY": 0}
PASS iphone14: income nav top unchanged (97, round-1) {"navTop": 97}
PASS iphone14: income CTA at round-1 (485) when not stuck, pinned when tall {"nextTop": 485, "nextBottom": 535, "panelTop": 618, "maxScroll": 0}
iphone14 SWIPE: [{"sel": " #g1", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 1036, "bottom": 1060, "left": 301, "right": 363}}, {"sel": "#g2", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 1445, "bottom": 1469, "left": 301, "right": 363}}, {"sel": "#g3", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 1882, "bottom": 1906, "left": 301, "right": 363}}, {"sel": "#g4", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 13, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 2319, "bottom": 2343, "left": 301, "right": 363}}]
PASS iphone14 #g1: swipe hint present+visible
PASS iphone14 #g1: swipe does not intersect <text> {"texts": 12}
PASS iphone14 #g1: swipe inside card bounds
PASS iphone14 #g2: swipe hint present+visible
PASS iphone14 #g2: swipe does not intersect <text> {"texts": 12}
PASS iphone14 #g2: swipe inside card bounds
PASS iphone14 #g3: swipe hint present+visible
PASS iphone14 #g3: swipe does not intersect <text> {"texts": 12}
PASS iphone14 #g3: swipe inside card bounds
PASS iphone14 #g4: swipe hint present+visible
PASS iphone14 #g4: swipe does not intersect <text> {"texts": 13}
PASS iphone14 #g4: swipe inside card bounds
PASS iphone14: swipe on all 4 charts
iphone14 HASH-ANCHOR: {"scrollY": 2662, "targetTop": 70, "hdrBottom": 55}
PASS iphone14: anchor scrollY > 0 {"scrollY": 2662}
PASS iphone14: #whatif clears sticky header {"targetTop": 70, "hdrBottom": 55}
pixel step0 @scrollY=0: {"scrollY": 0, "maxScroll": 161, "panelTop": 574, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 97, "bottom": 153}, "next": {"top": 520, "bottom": 570}, "nextTappable": true, "lastOptBottom": 630}
PASS pixel step0: next bottom <= panelTop-2 @0 {"nextBottom": 570, "panelTop": 574}
PASS pixel step0: next tappable @0
pixel step0 @mid: {"scrollY": 81, "maxScroll": 161, "panelTop": 574, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 16, "bottom": 72}, "next": {"top": 520, "bottom": 570}, "nextTappable": true, "lastOptBottom": 549}
PASS pixel step0: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 72, "panelTop": 574}
PASS pixel step0: next still pinned mid (nextBottom <= panelTop-2) {"nextBottom": 570, "panelTop": 574}
pixel step0 @max: {"scrollY": 161, "maxScroll": 161, "panelTop": 574, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": -64, "bottom": -8}, "next": {"top": 506, "bottom": 556}, "nextTappable": true, "lastOptBottom": 469}
PASS pixel step0: last opt bottom <= panelTop-2 @max {"lastOptBottom": 469, "panelTop": 574}
PASS pixel step0: next tappable @max
pixel step13-interests @scrollY=0: {"scrollY": 0, "maxScroll": 109, "panelTop": 574, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 97, "bottom": 153}, "next": {"top": 520, "bottom": 570}, "nextTappable": true, "lastOptBottom": 578}
PASS pixel step13-interests: next bottom <= panelTop-2 @0 {"nextBottom": 570, "panelTop": 574}
PASS pixel step13-interests: next tappable @0
pixel step13-interests @mid: {"scrollY": 55, "maxScroll": 109, "panelTop": 574, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 42, "bottom": 98}, "next": {"top": 520, "bottom": 570}, "nextTappable": true, "lastOptBottom": 523}
PASS pixel step13-interests: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 98, "panelTop": 574}
PASS pixel step13-interests: next still pinned mid (nextBottom <= panelTop-2) {"nextBottom": 570, "panelTop": 574}
pixel step13-interests @max: {"scrollY": 109, "maxScroll": 109, "panelTop": 574, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": -12, "bottom": 44}, "next": {"top": 506, "bottom": 556}, "nextTappable": true, "lastOptBottom": 469}
PASS pixel step13-interests: last opt bottom <= panelTop-2 @max {"lastOptBottom": 469, "panelTop": 574}
PASS pixel step13-interests: next tappable @max
pixel income-step3 (doc coords): {"navTop": 97, "navBottom": 153, "navPos": "sticky", "nextTop": 485, "nextBottom": 535, "panelTop": 574, "maxScroll": 0, "scrollY": 0}
PASS pixel: income nav top unchanged (97, round-1) {"navTop": 97}
PASS pixel: income CTA at round-1 (485) when not stuck, pinned when tall {"nextTop": 485, "nextBottom": 535, "panelTop": 574, "maxScroll": 0}
pixel SWIPE: [{"sel": " #g1", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 1085, "bottom": 1109, "left": 271, "right": 333}}, {"sel": "#g2", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 1522, "bottom": 1545, "left": 271, "right": 333}}, {"sel": "#g3", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 1959, "bottom": 1982, "left": 271, "right": 333}}, {"sel": "#g4", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 13, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 2428, "bottom": 2451, "left": 271, "right": 333}}]
PASS pixel #g1: swipe hint present+visible
PASS pixel #g1: swipe does not intersect <text> {"texts": 12}
PASS pixel #g1: swipe inside card bounds
PASS pixel #g2: swipe hint present+visible
PASS pixel #g2: swipe does not intersect <text> {"texts": 12}
PASS pixel #g2: swipe inside card bounds
PASS pixel #g3: swipe hint present+visible
PASS pixel #g3: swipe does not intersect <text> {"texts": 12}
PASS pixel #g3: swipe inside card bounds
PASS pixel #g4: swipe hint present+visible
PASS pixel #g4: swipe does not intersect <text> {"texts": 13}
PASS pixel #g4: swipe inside card bounds
PASS pixel: swipe on all 4 charts
pixel HASH-ANCHOR: {"scrollY": 2771, "targetTop": 70, "hdrBottom": 55}
PASS pixel: anchor scrollY > 0 {"scrollY": 2771}
PASS pixel: #whatif clears sticky header {"targetTop": 70, "hdrBottom": 55}
se step0 @scrollY=0: {"scrollY": 0, "maxScroll": 295, "panelTop": 474, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 97, "bottom": 153}, "next": {"top": 420, "bottom": 470}, "nextTappable": true, "lastOptBottom": 664}
PASS se step0: next bottom <= panelTop-2 @0 {"nextBottom": 470, "panelTop": 474}
PASS se step0: next tappable @0
se step0 @mid: {"scrollY": 148, "maxScroll": 295, "panelTop": 474, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": -51, "bottom": 5}, "next": {"top": 420, "bottom": 470}, "nextTappable": true, "lastOptBottom": 516}
PASS se step0: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 5, "panelTop": 474}
PASS se step0: next still pinned mid (nextBottom <= panelTop-2) {"nextBottom": 470, "panelTop": 474}
se step0 @max: {"scrollY": 295, "maxScroll": 295, "panelTop": 474, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": -198, "bottom": -142}, "next": {"top": 406, "bottom": 456}, "nextTappable": true, "lastOptBottom": 369}
PASS se step0: last opt bottom <= panelTop-2 @max {"lastOptBottom": 369, "panelTop": 474}
PASS se step0: next tappable @max
se step13-interests @scrollY=0: {"scrollY": 0, "maxScroll": 240, "panelTop": 474, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": 97, "bottom": 153}, "next": {"top": 420, "bottom": 470}, "nextTappable": true, "lastOptBottom": 609}
PASS se step13-interests: next bottom <= panelTop-2 @0 {"nextBottom": 470, "panelTop": 474}
PASS se step13-interests: next tappable @0
se step13-interests @mid: {"scrollY": 120, "maxScroll": 240, "panelTop": 474, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": -23, "bottom": 33}, "next": {"top": 420, "bottom": 470}, "nextTappable": true, "lastOptBottom": 489}
PASS se step13-interests: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 33, "panelTop": 474}
PASS se step13-interests: next still pinned mid (nextBottom <= panelTop-2) {"nextBottom": 470, "panelTop": 474}
se step13-interests @max: {"scrollY": 240, "maxScroll": 240, "panelTop": 474, "sheetH": 218, "sheetVar": "218px", "scrPadBottom": "232px", "navPos": "sticky", "nav": {"top": -143, "bottom": -87}, "next": {"top": 406, "bottom": 456}, "nextTappable": true, "lastOptBottom": 369}
PASS se step13-interests: last opt bottom <= panelTop-2 @max {"lastOptBottom": 369, "panelTop": 474}
PASS se step13-interests: next tappable @max
se income-step3 (doc coords): {"navTop": 97, "navBottom": 153, "navPos": "sticky", "nextTop": 420, "nextBottom": 470, "panelTop": 474, "maxScroll": 79, "scrollY": 0}
PASS se: income nav top unchanged (97, round-1) {"navTop": 97}
PASS se: income CTA at round-1 (485) when not stuck, pinned when tall {"nextTop": 420, "nextBottom": 470, "panelTop": 474, "maxScroll": 79}
se SWIPE: [{"sel": " #g1", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 1108, "bottom": 1132, "left": 231, "right": 293}}, {"sel": "#g2", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 1566, "bottom": 1590, "left": 231, "right": 293}}, {"sel": "#g3", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 12, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 2035, "bottom": 2059, "left": 231, "right": 293}}, {"sel": "#g4", "hasSwipe": true, "inHeader": true, "swipeVisible": true, "swipeText": "swipe \u2192", "texts": 13, "svg": true, "intersectsText": false, "swipeInsideCard": true, "swipe": {"top": 2504, "bottom": 2528, "left": 231, "right": 293}}]
PASS se #g1: swipe hint present+visible
PASS se #g1: swipe does not intersect <text> {"texts": 12}
PASS se #g1: swipe inside card bounds
PASS se #g2: swipe hint present+visible
PASS se #g2: swipe does not intersect <text> {"texts": 12}
PASS se #g2: swipe inside card bounds
PASS se #g3: swipe hint present+visible
PASS se #g3: swipe does not intersect <text> {"texts": 12}
PASS se #g3: swipe inside card bounds
PASS se #g4: swipe hint present+visible
PASS se #g4: swipe does not intersect <text> {"texts": 13}
PASS se #g4: swipe inside card bounds
PASS se: swipe on all 4 charts
se HASH-ANCHOR: {"scrollY": 2896, "targetTop": 70, "hdrBottom": 55}
PASS se: anchor scrollY > 0 {"scrollY": 2896}
PASS se: #whatif clears sticky header {"targetTop": 70, "hdrBottom": 55}
desk1280 DESKTOP-QUIZ: {"navPos": "static", "scrPadBottom": "80px", "navTop": 107}
PASS desk1280: .step-nav position static {"navPos": "static"}
PASS desk1280: .scr padding-bottom 80px {"pad": "80px"}
desk1280 DESKTOP-SWIPE: [{"sel": "#g1", "present": true, "visible": false}, {"sel": "#g2", "present": true, "visible": false}, {"sel": "#g3", "present": true, "visible": false}, {"sel": "#g4", "present": true, "visible": false}]
PASS desk1280: desktop swipe hidden [{"sel": "#g1", "present": true, "visible": false}, {"sel": "#g2", "present": true, "visible": false}, {"sel": "#g3", "present": true, "visible": false}, {"sel": "#g4", "present": true, "visible": false}]
VERIFY2-OK
+10
View File
@@ -0,0 +1,10 @@
iphone14 STEP0: {"innerH": 844, "docH": 914, "maxScroll": 70, "panelTop": 618, "sheetH": 218, "next": {"top": 667, "bottom": 717, "h": 50}, "nextBehindSheet": true, "nextTappable": false, "lastOptText": "Nunavut", "lastOpt": {"top": 587, "bottom": 630, "h": 43}, "lastOptBehindSheet": true}
iphone14 G2: {"boxH": 285, "svgH": 236, "svgFitsVertically": true, "svgBottomMinusBoxBottom": -35, "bottomTick": ["80", "85", "90", "age"]}
iphone14 HASH-ANCHOR: {"targetTop": 1382, "hdrBottom": 55, "covered": false}
pixel STEP0: {"innerH": 800, "docH": 914, "maxScroll": 114, "panelTop": 574, "sheetH": 218, "next": {"top": 667, "bottom": 717, "h": 50}, "nextBehindSheet": true, "nextTappable": false, "lastOptText": "Nunavut", "lastOpt": {"top": 587, "bottom": 630, "h": 43}, "lastOptBehindSheet": true}
pixel G2: {"boxH": 285, "svgH": 236, "svgFitsVertically": true, "svgBottomMinusBoxBottom": -35, "bottomTick": ["80", "85", "90", "age"]}
pixel HASH-ANCHOR: {"targetTop": 1393, "hdrBottom": 55, "covered": false}
se STEP0: {"innerH": 700, "docH": 948, "maxScroll": 248, "panelTop": 474, "sheetH": 218, "next": {"top": 701, "bottom": 751, "h": 50}, "nextBehindSheet": true, "nextTappable": false, "lastOptText": "Nunavut", "lastOpt": {"top": 621, "bottom": 664, "h": 43}, "lastOptBehindSheet": true}
se G2: {"boxH": 317, "svgH": 236, "svgFitsVertically": true, "svgBottomMinusBoxBottom": -67, "bottomTick": ["80", "85", "90", "age"]}
se HASH-ANCHOR: {"targetTop": 1386, "hdrBottom": 55, "covered": false}
MEASURE2-DONE
Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

+103
View File
@@ -0,0 +1,103 @@
iphone14 step0 @scrollY=0: {"scrollY": 0, "maxScroll": 70, "panelTop": 618, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 667, "bottom": 717}, "nextTappable": false, "lastOptBottom": 630}
FAIL iphone14 step0: next bottom <= panelTop-2 @0 {"nextBottom": 717, "panelTop": 618}
FAIL iphone14 step0: next tappable @0
iphone14 step0 @mid: {"scrollY": 0, "maxScroll": 70, "panelTop": 618, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 667, "bottom": 717}, "nextTappable": false, "lastOptBottom": 630}
PASS iphone14 step0: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 141, "panelTop": 618}
iphone14 step0 @max: {"scrollY": 0, "maxScroll": 70, "panelTop": 618, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 667, "bottom": 717}, "nextTappable": false, "lastOptBottom": 630}
FAIL iphone14 step0: last opt bottom <= panelTop-2 @max {"lastOptBottom": 630, "panelTop": 618}
iphone14 step13-interests @scrollY=0: {"scrollY": 0, "maxScroll": 0, "panelTop": 618, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 556, "bottom": 606}, "nextTappable": true, "lastOptBottom": 519}
PASS iphone14 step13-interests: next bottom <= panelTop-2 @0 {"nextBottom": 606, "panelTop": 618}
PASS iphone14 step13-interests: next tappable @0
iphone14 step13-interests @mid: {"scrollY": 0, "maxScroll": 0, "panelTop": 618, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 556, "bottom": 606}, "nextTappable": true, "lastOptBottom": 519}
PASS iphone14 step13-interests: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 141, "panelTop": 618}
iphone14 step13-interests @max: {"scrollY": 0, "maxScroll": 0, "panelTop": 618, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 556, "bottom": 606}, "nextTappable": true, "lastOptBottom": 519}
PASS iphone14 step13-interests: last opt bottom <= panelTop-2 @max {"lastOptBottom": 519, "panelTop": 618}
iphone14 income-step3 (doc coords): {"navTop": 97, "navBottom": 141, "navPos": "static", "scrollY": 0}
iphone14 SWIPE: [{"sel": " #g1", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g2", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g3", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g4", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 13, "svg": true}]
FAIL iphone14 #g1: swipe hint present+visible
FAIL iphone14 #g1: swipe does not intersect <text> {"texts": 12}
FAIL iphone14 #g1: swipe inside card bounds
FAIL iphone14 #g2: swipe hint present+visible
FAIL iphone14 #g2: swipe does not intersect <text> {"texts": 12}
FAIL iphone14 #g2: swipe inside card bounds
FAIL iphone14 #g3: swipe hint present+visible
FAIL iphone14 #g3: swipe does not intersect <text> {"texts": 12}
FAIL iphone14 #g3: swipe inside card bounds
FAIL iphone14 #g4: swipe hint present+visible
FAIL iphone14 #g4: swipe does not intersect <text> {"texts": 13}
FAIL iphone14 #g4: swipe inside card bounds
FAIL iphone14: swipe on all 4 charts
iphone14 HASH-ANCHOR: {"scrollY": 2562, "targetTop": 56, "hdrBottom": 55}
PASS iphone14: anchor scrollY > 0 {"scrollY": 2562}
PASS iphone14: #whatif clears sticky header {"targetTop": 56, "hdrBottom": 55}
pixel step0 @scrollY=0: {"scrollY": 0, "maxScroll": 114, "panelTop": 574, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 667, "bottom": 717}, "nextTappable": false, "lastOptBottom": 630}
FAIL pixel step0: next bottom <= panelTop-2 @0 {"nextBottom": 717, "panelTop": 574}
FAIL pixel step0: next tappable @0
pixel step0 @mid: {"scrollY": 0, "maxScroll": 114, "panelTop": 574, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 667, "bottom": 717}, "nextTappable": false, "lastOptBottom": 630}
PASS pixel step0: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 141, "panelTop": 574}
pixel step0 @max: {"scrollY": 0, "maxScroll": 114, "panelTop": 574, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 667, "bottom": 717}, "nextTappable": false, "lastOptBottom": 630}
FAIL pixel step0: last opt bottom <= panelTop-2 @max {"lastOptBottom": 630, "panelTop": 574}
pixel step13-interests @scrollY=0: {"scrollY": 0, "maxScroll": 62, "panelTop": 574, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 615, "bottom": 665}, "nextTappable": false, "lastOptBottom": 578}
FAIL pixel step13-interests: next bottom <= panelTop-2 @0 {"nextBottom": 665, "panelTop": 574}
FAIL pixel step13-interests: next tappable @0
pixel step13-interests @mid: {"scrollY": 0, "maxScroll": 62, "panelTop": 574, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 615, "bottom": 665}, "nextTappable": false, "lastOptBottom": 578}
PASS pixel step13-interests: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 141, "panelTop": 574}
pixel step13-interests @max: {"scrollY": 0, "maxScroll": 62, "panelTop": 574, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 615, "bottom": 665}, "nextTappable": false, "lastOptBottom": 578}
FAIL pixel step13-interests: last opt bottom <= panelTop-2 @max {"lastOptBottom": 578, "panelTop": 574}
pixel income-step3 (doc coords): {"navTop": 97, "navBottom": 141, "navPos": "static", "scrollY": 0}
pixel SWIPE: [{"sel": " #g1", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g2", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g3", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g4", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 13, "svg": true}]
FAIL pixel #g1: swipe hint present+visible
FAIL pixel #g1: swipe does not intersect <text> {"texts": 12}
FAIL pixel #g1: swipe inside card bounds
FAIL pixel #g2: swipe hint present+visible
FAIL pixel #g2: swipe does not intersect <text> {"texts": 12}
FAIL pixel #g2: swipe inside card bounds
FAIL pixel #g3: swipe hint present+visible
FAIL pixel #g3: swipe does not intersect <text> {"texts": 12}
FAIL pixel #g3: swipe inside card bounds
FAIL pixel #g4: swipe hint present+visible
FAIL pixel #g4: swipe does not intersect <text> {"texts": 13}
FAIL pixel #g4: swipe inside card bounds
FAIL pixel: swipe on all 4 charts
pixel HASH-ANCHOR: {"scrollY": 2699, "targetTop": -0.0, "hdrBottom": 55}
PASS pixel: anchor scrollY > 0 {"scrollY": 2699}
FAIL pixel: #whatif clears sticky header {"targetTop": -0.0, "hdrBottom": 55}
se step0 @scrollY=0: {"scrollY": 0, "maxScroll": 248, "panelTop": 474, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 701, "bottom": 751}, "nextTappable": false, "lastOptBottom": 664}
FAIL se step0: next bottom <= panelTop-2 @0 {"nextBottom": 751, "panelTop": 474}
FAIL se step0: next tappable @0
se step0 @mid: {"scrollY": 0, "maxScroll": 248, "panelTop": 474, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 701, "bottom": 751}, "nextTappable": false, "lastOptBottom": 664}
PASS se step0: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 141, "panelTop": 474}
se step0 @max: {"scrollY": 0, "maxScroll": 248, "panelTop": 474, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 97, "bottom": 141}, "next": {"top": 701, "bottom": 751}, "nextTappable": false, "lastOptBottom": 664}
FAIL se step0: last opt bottom <= panelTop-2 @max {"lastOptBottom": 664, "panelTop": 474}
se step13-interests @scrollY=0: {"scrollY": 3, "maxScroll": 193, "panelTop": 474, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 94, "bottom": 138}, "next": {"top": 643, "bottom": 693}, "nextTappable": false, "lastOptBottom": 606}
FAIL se step13-interests: next bottom <= panelTop-2 @0 {"nextBottom": 693, "panelTop": 474}
FAIL se step13-interests: next tappable @0
se step13-interests @mid: {"scrollY": 3, "maxScroll": 193, "panelTop": 474, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 94, "bottom": 138}, "next": {"top": 643, "bottom": 693}, "nextTappable": false, "lastOptBottom": 606}
PASS se step13-interests: nav pinned mid (navBottom <= panelTop-2) {"navBottom": 138, "panelTop": 474}
se step13-interests @max: {"scrollY": 3, "maxScroll": 193, "panelTop": 474, "sheetH": 218, "sheetVar": "", "scrPadBottom": "185px", "navPos": "static", "nav": {"top": 94, "bottom": 138}, "next": {"top": 643, "bottom": 693}, "nextTappable": false, "lastOptBottom": 606}
FAIL se step13-interests: last opt bottom <= panelTop-2 @max {"lastOptBottom": 606, "panelTop": 474}
se income-step3 (doc coords): {"navTop": 97, "navBottom": 141, "navPos": "static", "scrollY": 9}
se SWIPE: [{"sel": " #g1", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g2", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g3", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 12, "svg": true}, {"sel": "#g4", "hasSwipe": false, "inHeader": false, "swipeVisible": false, "swipeText": null, "texts": 13, "svg": true}]
FAIL se #g1: swipe hint present+visible
FAIL se #g1: swipe does not intersect <text> {"texts": 12}
FAIL se #g1: swipe inside card bounds
FAIL se #g2: swipe hint present+visible
FAIL se #g2: swipe does not intersect <text> {"texts": 12}
FAIL se #g2: swipe inside card bounds
FAIL se #g3: swipe hint present+visible
FAIL se #g3: swipe does not intersect <text> {"texts": 12}
FAIL se #g3: swipe inside card bounds
FAIL se #g4: swipe hint present+visible
FAIL se #g4: swipe does not intersect <text> {"texts": 13}
FAIL se #g4: swipe inside card bounds
FAIL se: swipe on all 4 charts
se HASH-ANCHOR: {"scrollY": 2810, "targetTop": -0.0, "hdrBottom": 55}
PASS se: anchor scrollY > 0 {"scrollY": 2810}
FAIL se: #whatif clears sticky header {"targetTop": -0.0, "hdrBottom": 55}
desk1280 DESKTOP-QUIZ: {"navPos": "static", "scrPadBottom": "80px", "navTop": 107}
PASS desk1280: .step-nav position static {"navPos": "static"}
PASS desk1280: .scr padding-bottom 80px {"pad": "80px"}
desk1280 DESKTOP-SWIPE: [{"sel": "#g1", "present": false, "visible": false}, {"sel": "#g2", "present": false, "visible": false}, {"sel": "#g3", "present": false, "visible": false}, {"sel": "#g4", "present": false, "visible": false}]
PASS desk1280: desktop swipe hidden [{"sel": "#g1", "present": false, "visible": false}, {"sel": "#g2", "present": false, "visible": false}, {"sel": "#g3", "present": false, "visible": false}, {"sel": "#g4", "present": false, "visible": false}]
VERIFY2-FAIL (56): iphone14 step0: next bottom <= panelTop-2 @0; iphone14 step0: next tappable @0; iphone14 step0: last opt bottom <= panelTop-2 @max; iphone14 #g1: swipe hint present+visible; iphone14 #g1: swipe does not intersect <text>; iphone14 #g1: swipe inside card bounds; iphone14 #g2: swipe hint present+visible; iphone14 #g2: swipe does not intersect <text>; iphone14 #g2: swipe inside card bounds; iphone14 #g3: swipe hint present+visible; iphone14 #g3: swipe does not intersect <text>; iphone14 #g3: swipe inside card bounds; iphone14 #g4: swipe hint present+visible; iphone14 #g4: swipe does not intersect <text>; iphone14 #g4: swipe inside card bounds; iphone14: swipe on all 4 charts; pixel step0: next bottom <= panelTop-2 @0; pixel step0: next tappable @0; pixel step0: last opt bottom <= panelTop-2 @max; pixel step13-interests: next bottom <= panelTop-2 @0; pixel step13-interests: next tappable @0; pixel step13-interests: last opt bottom <= panelTop-2 @max; pixel #g1: swipe hint present+visible; pixel #g1: swipe does not intersect <text>; pixel #g1: swipe inside card bounds; pixel #g2: swipe hint present+visible; pixel #g2: swipe does not intersect <text>; pixel #g2: swipe inside card bounds; pixel #g3: swipe hint present+visible; pixel #g3: swipe does not intersect <text>; pixel #g3: swipe inside card bounds; pixel #g4: swipe hint present+visible; pixel #g4: swipe does not intersect <text>; pixel #g4: swipe inside card bounds; pixel: swipe on all 4 charts; pixel: #whatif clears sticky header; se step0: next bottom <= panelTop-2 @0; se step0: next tappable @0; se step0: last opt bottom <= panelTop-2 @max; se step13-interests: next bottom <= panelTop-2 @0; se step13-interests: next tappable @0; se step13-interests: last opt bottom <= panelTop-2 @max; se #g1: swipe hint present+visible; se #g1: swipe does not intersect <text>; se #g1: swipe inside card bounds; se #g2: swipe hint present+visible; se #g2: swipe does not intersect <text>; se #g2: swipe inside card bounds; se #g3: swipe hint present+visible; se #g3: swipe does not intersect <text>; se #g3: swipe inside card bounds; se #g4: swipe hint present+visible; se #g4: swipe does not intersect <text>; se #g4: swipe inside card bounds; se: swipe on all 4 charts; se: #whatif clears sticky header
+127
View File
@@ -0,0 +1,127 @@
"""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')
+207
View File
@@ -0,0 +1,207 @@
"""Round-2 verification: MOBILE-FIX-2.md Issue 1/2/3 assertions, measured on the LIVE site.
Runs step-0 + tall-step checks (Issue 1), swipe-vs-ticks (Issue 2), anchor scroll (Issue 3),
short-step nav-top baseline, and a desktop 1280 spot-check. Prints VERIFY2-OK / VERIFY2-FAIL.
Usage: python3 audit/verify2.py > audit/mobile-<before|after>2/verify-round2.txt 2>&1
"""
import json, os, sys
from playwright.sync_api import sync_playwright
CHROME = '/home/ubuntu/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome'
BASE = os.environ.get('RP_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'),
('desk1280', 1280, 900, 1.0, 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'),
]
fails = []
def chk(name, cond, extra=None):
print((' PASS ' if cond else ' FAIL ') + name + (' ' + json.dumps(extra) if extra is not None else ''))
if not cond:
fails.append(name)
def set_state(page, screen, step):
page.evaluate("([ls, a, screen, step]) => localStorage.setItem(ls, JSON.stringify({a: a, step: step, screen: screen}))",
[LS, ANSWERS, screen, step])
def scroll_to(page, frac):
page.evaluate("""(frac) => {
const max = document.documentElement.scrollHeight - window.innerHeight;
window.scrollTo({top: Math.round(max * frac), behavior: 'instant'});
}""", frac)
page.wait_for_timeout(350) # scroll-behavior:smooth — let it settle before measuring
MEASURE_JS = """() => {
const rc = e => { const b = e.getBoundingClientRect(); return {top: Math.round(b.top), bottom: Math.round(b.bottom)}; };
const panel = document.querySelector('.panel');
const nav = document.querySelector('.step-nav');
const next = document.querySelector('#btn-next');
const opts = [...document.querySelectorAll('.opt')];
const last = opts[opts.length - 1];
const pTop = Math.round(panel.getBoundingClientRect().top);
const nc = next.getBoundingClientRect();
const elAt = document.elementFromPoint(nc.left + nc.width / 2, nc.top + nc.height / 2);
return {
scrollY: Math.round(window.scrollY),
maxScroll: Math.round(document.documentElement.scrollHeight - window.innerHeight),
panelTop: pTop,
sheetH: Math.round(panel.getBoundingClientRect().height),
sheetVar: getComputedStyle(document.documentElement).getPropertyValue('--sheet-h').trim(),
scrPadBottom: getComputedStyle(document.querySelector('#scr-quiz')).paddingBottom,
navPos: getComputedStyle(nav).position,
nav: rc(nav), next: rc(next),
nextTappable: !!(elAt && elAt.closest && elAt.closest('#btn-next')),
lastOptBottom: last ? Math.round(last.getBoundingClientRect().bottom) : null
};
}"""
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:
mobile = dev != 'desk1280'
ctx = browser.new_context(viewport={'width': w, 'height': h}, device_scale_factor=dsf,
is_mobile=mobile, has_touch=mobile, user_agent=ua)
page = ctx.new_page()
page.goto(BASE, wait_until='networkidle')
# ---------------- Issue 1: quiz, tall steps (step 0 = province grid, step 13 = interests) ----------
for step, tag in ((0, 'step0'), (13, 'step13-interests')):
if not mobile:
break # Issue 1 assertions are mobile; desktop checked separately below
set_state(page, 'quiz', step)
page.reload(wait_until='networkidle'); page.wait_for_timeout(700)
scroll_to(page, 0.0)
g0 = page.evaluate(MEASURE_JS)
print(f'{dev} {tag} @scrollY=0:', json.dumps(g0))
chk(f'{dev} {tag}: next bottom <= panelTop-2 @0', g0['next']['bottom'] <= g0['panelTop'] - 2,
{'nextBottom': g0['next']['bottom'], 'panelTop': g0['panelTop']})
chk(f'{dev} {tag}: next tappable @0', g0['nextTappable'])
scroll_to(page, 0.5)
gm = page.evaluate(MEASURE_JS)
print(f'{dev} {tag} @mid:', json.dumps(gm))
chk(f'{dev} {tag}: nav pinned mid (navBottom <= panelTop-2)',
gm['nav']['bottom'] <= gm['panelTop'] - 2, {'navBottom': gm['nav']['bottom'], 'panelTop': gm['panelTop']})
chk(f'{dev} {tag}: next still pinned mid (nextBottom <= panelTop-2)',
gm['next']['bottom'] <= gm['panelTop'] - 2, {'nextBottom': gm['next']['bottom'], 'panelTop': gm['panelTop']})
scroll_to(page, 1.0)
gmax = page.evaluate(MEASURE_JS)
print(f'{dev} {tag} @max:', json.dumps(gmax))
chk(f'{dev} {tag}: last opt bottom <= panelTop-2 @max',
gmax['lastOptBottom'] <= gmax['panelTop'] - 2, {'lastOptBottom': gmax['lastOptBottom'], 'panelTop': gmax['panelTop']})
chk(f'{dev} {tag}: next tappable @max', gmax['nextTappable'])
# ---------------- Issue 1 (short step): income (quiz step 3 single) — no shift when not stuck --------
# Round-1 (pre-fix, audit/mobile-before2/verify-round2.txt): nav doc-top = 97,
# #btn-next top = 460 / center = 485 on all 3 widths. Round 2 must reproduce that exactly.
if mobile:
set_state(page, 'quiz', 3)
page.reload(wait_until='networkidle'); page.wait_for_timeout(600)
gi = page.evaluate("""() => {
const nav = document.querySelector('.step-nav');
const nx = document.querySelector('#btn-next');
const b = nav.getBoundingClientRect(), n = nx.getBoundingClientRect();
return {navTop: Math.round(b.top + window.scrollY), navBottom: Math.round(b.bottom + window.scrollY),
navPos: getComputedStyle(nav).position,
nextTop: Math.round(n.top + window.scrollY), nextBottom: Math.round(n.bottom),
panelTop: Math.round(document.querySelector('.panel').getBoundingClientRect().top),
maxScroll: Math.round(document.documentElement.scrollHeight - window.innerHeight),
scrollY: Math.round(window.scrollY)};
}""")
print(f'{dev} income-step3 (doc coords):', json.dumps(gi))
chk(f'{dev}: income nav top unchanged (97, round-1)', gi['navTop'] == 97, {'navTop': gi['navTop']})
# Spec: "nav top ~485 unchanged … visually nothing changes on short steps". True round-1
# CTA-row top (measured on git HEAD copy at 390/360/320) = 485. Not-stuck rendering must
# reproduce it exactly. At 320 the income step is TALL (docH > vh), so round-2 pins the
# CTA above the sheet (that IS the issue-1 fix) — accept the pinned position there.
unstuck = gi['nextTop'] == 485
pinned = gi['panelTop'] - 16 <= gi['nextBottom'] <= gi['panelTop'] - 2
chk(f'{dev}: income CTA at round-1 (485) when not stuck, pinned when tall',
unstuck or pinned, {'nextTop': gi['nextTop'], 'nextBottom': gi['nextBottom'],
'panelTop': gi['panelTop'], 'maxScroll': gi['maxScroll']})
# ---------------- Issue 2: swipe hint in chart header, no text intersection ------------------------
if mobile:
set_state(page, 'results', 0)
page.reload(wait_until='networkidle'); page.wait_for_timeout(1500)
sw = page.evaluate("""() => [' #g1','#g2','#g3','#g4'].map(sel => {
const box = document.querySelector(sel.trim());
if (!box) return {sel, missing: true};
const span = box.querySelector('.swipe');
const hdr = box.querySelector('.chart-h');
const svg = box.querySelector('svg');
const texts = svg ? [...svg.querySelectorAll('text')] : [];
const out = {sel, hasSwipe: !!span, inHeader: !!(span && hdr && hdr.contains(span)),
swipeVisible: !!(span && span.offsetParent),
swipeText: span ? span.textContent.trim() : null,
texts: texts.length, svg: !!svg};
if (span && texts.length) {
const r = span.getBoundingClientRect();
out.intersectsText = texts.some(t => { const b = t.getBoundingClientRect();
return !(r.right <= b.left || r.left >= b.right || r.bottom <= b.top || r.top >= b.bottom); });
const br = box.getBoundingClientRect();
out.swipeInsideCard = r.left >= br.left - 1 && r.right <= br.right + 1 && r.top >= br.top - 1 && r.bottom <= br.bottom + 1;
out.swipe = {top: Math.round(r.top), bottom: Math.round(r.bottom), left: Math.round(r.left), right: Math.round(r.right)};
}
return out;
})""")
print(f'{dev} SWIPE:', json.dumps(sw))
for o in sw:
chk(f'{dev} {o["sel"]}: swipe hint present+visible', o.get('hasSwipe') and o.get('swipeVisible') and o.get('inHeader'))
chk(f'{dev} {o["sel"]}: swipe does not intersect <text>', o.get('intersectsText') is False,
{'texts': o.get('texts')})
chk(f'{dev} {o["sel"]}: swipe inside card bounds', o.get('swipeInsideCard') is True)
chk(f'{dev}: swipe on all 4 charts', all(o.get('hasSwipe') for o in sw))
# ---------------- Issue 3: anchor under sticky header ------------------------------------------
ha = page.evaluate("""() => {
location.hash = '#whatif';
return new Promise(res => setTimeout(() => {
const t = document.querySelector('#whatif'), hdr = document.querySelector('.hdr');
if (!t) return res({missing: true});
const tr = t.getBoundingClientRect(), hr = hdr.getBoundingClientRect();
res({scrollY: Math.round(window.scrollY), targetTop: Math.round(tr.top), hdrBottom: Math.round(hr.bottom)});
}, 2000));
}""")
print(f'{dev} HASH-ANCHOR:', json.dumps(ha))
chk(f'{dev}: anchor scrollY > 0', ha.get('scrollY', 0) > 0, {'scrollY': ha.get('scrollY')})
chk(f'{dev}: #whatif clears sticky header', ha.get('targetTop', -99) >= ha.get('hdrBottom', 999) - 1,
{'targetTop': ha.get('targetTop'), 'hdrBottom': ha.get('hdrBottom')})
else:
# ---------------- Desktop spot-check (1280): static nav, 80px scr padding, hidden hint ----------
set_state(page, 'quiz', 3)
page.reload(wait_until='networkidle'); page.wait_for_timeout(600)
d = page.evaluate("""() => ({
navPos: getComputedStyle(document.querySelector('.step-nav')).position,
scrPadBottom: getComputedStyle(document.querySelector('#scr-quiz')).paddingBottom,
navTop: Math.round(document.querySelector('.step-nav').getBoundingClientRect().top + window.scrollY)
})""")
print(f'{dev} DESKTOP-QUIZ:', json.dumps(d))
chk(f'{dev}: .step-nav position static', d['navPos'] == 'static', {'navPos': d['navPos']})
chk(f'{dev}: .scr padding-bottom 80px', d['scrPadBottom'] == '80px', {'pad': d['scrPadBottom']})
set_state(page, 'results', 0)
page.reload(wait_until='networkidle'); page.wait_for_timeout(1500)
d2 = page.evaluate("""() => [' #g1','#g2','#g3','#g4'].map(s => {
const sp = document.querySelector(s.trim()).querySelector('.swipe');
return {sel: s.trim(), present: !!sp, visible: !!(sp && sp.offsetParent)};
})""")
print(f'{dev} DESKTOP-SWIPE:', json.dumps(d2))
chk(f'{dev}: desktop swipe hidden', all((not o['visible']) for o in d2), d2)
ctx.close()
browser.close()
print('\nVERIFY2-' + ('OK' if not fails else f'FAIL ({len(fails)}): ' + '; '.join(fails)))
sys.exit(0 if not fails else 1)