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.
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')
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 95 KiB |
@@ -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
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 101 KiB |
@@ -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
|
||||
@@ -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')
|
||||
@@ -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)
|
||||