round 4: app-shell quiz screen at <=700px — #step-body is the only scroller, #btn-next/#panel become the static bottom cluster; revert FR .opt override (keep f4b5698); + capture/verify/pixdiff/offlan audit tooling and before/after evidence; closes QUIZ-APPSHELL-FIX.md
This commit is contained in:
1 parent
f4b5698272
commit
a5923da47c
84 files changed
+10743
-4
No files matched your search
@@ -0,0 +1,139 @@
|
||||
"""App-shell round-4 capture: desktop 1280x900 EN+FR (province + tall vision step + results)
|
||||
for the pixel-diff baseline, and mobile 320/360/390 EN+FR quiz shots + geometry
|
||||
(btn-next / panel / .opt rects, coverage, doc scroll) for the before/after record.
|
||||
|
||||
Usage: python3 audit/appshell_capture.py BASE OUTDESK OUTMOB TAG
|
||||
e.g. python3 audit/appshell_capture.py http://127.0.0.1:8899/ audit/appshell-baseline audit/appshell-before before
|
||||
"""
|
||||
import json, os, sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
BASE = sys.argv[1].rstrip('/') + '/'
|
||||
OUTD = sys.argv[2]
|
||||
OUTM = sys.argv[3]
|
||||
TAG = sys.argv[4]
|
||||
os.makedirs(OUTD, exist_ok=True)
|
||||
os.makedirs(OUTM, exist_ok=True)
|
||||
CHROME = '/home/ubuntu/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome'
|
||||
LS = 'retire-planner-v1'
|
||||
|
||||
SINGLE = {
|
||||
"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"]
|
||||
}
|
||||
COUPLE = dict(SINGLE, household="couple",
|
||||
partner={"age": 43, "income": 62000, "rrsp": 120000, "tfsa": 30000, "nonreg": 15000, "monthlyContribution": 300})
|
||||
|
||||
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'),
|
||||
]
|
||||
# steps to capture on mobile: province (grid, 13 opts) + vision (interests chips, last step of 14)
|
||||
MSTEPS = [('province', 0), ('vision', 13)]
|
||||
|
||||
GEO_JS = """() => {
|
||||
const r = e => { const b = e.getBoundingClientRect(); return {l:+b.left.toFixed(1), t:+b.top.toFixed(1), r:+b.right.toFixed(1), b:+b.bottom.toFixed(1), w:+b.width.toFixed(1), h:+b.height.toFixed(1)}; };
|
||||
const g = s => document.querySelector(s);
|
||||
const inter = (a, b) => Math.max(0, Math.min(a.r, b.r) - Math.max(a.l, b.l)) * Math.max(0, Math.min(a.b, b.b) - Math.max(a.t, b.t));
|
||||
const next = g('#btn-next'), panel = g('#panel'), hdr = g('.hdr'), sb = g('#step-body');
|
||||
const nr = next ? r(next) : null, pr = panel ? r(panel) : null, hr = hdr ? r(hdr) : null;
|
||||
const opts = [...document.querySelectorAll('#step-body .opt')].map(e => ({txt: e.textContent.trim().slice(0, 22), rect: r(e)}));
|
||||
const coverage = nr ? opts.filter(o => inter(nr, o.rect) > 0).map(o => ({
|
||||
txt: o.txt, optAreaPct: +(inter(nr, o.rect) / (o.rect.w * o.rect.h) * 100).toFixed(1), opt: o.rect})) : [];
|
||||
return {
|
||||
vw: window.innerWidth, vh: window.innerHeight,
|
||||
docSH: document.documentElement.scrollHeight, docCH: document.documentElement.clientHeight,
|
||||
bodySH: document.body.scrollHeight, winScrollY: window.scrollY,
|
||||
hdr: hr, next: nr, panel: pr,
|
||||
stepBody: sb ? {...r(sb), scrollTop: +sb.scrollTop.toFixed(1), scrollH: sb.scrollHeight, clientH: sb.clientHeight} : null,
|
||||
nOpts: opts.length, lastOpt: opts.length ? opts[opts.length - 1].rect : null,
|
||||
coverage,
|
||||
lang: document.documentElement.lang,
|
||||
panelPos: panel ? getComputedStyle(panel).position : null,
|
||||
nextPos: next ? getComputedStyle(next).position : null,
|
||||
scrH: (() => { const e = g('#scr-quiz'); return e ? getComputedStyle(e).height : null; })(),
|
||||
};
|
||||
}"""
|
||||
|
||||
|
||||
def set_state(page, screen, step, lang, a=None):
|
||||
page.evaluate("""([ls, a, screen, step, lang]) => {
|
||||
localStorage.setItem(ls, JSON.stringify({a: a, step: step, screen: screen, lang: lang}));
|
||||
}""", [LS, a or SINGLE, screen, step, lang])
|
||||
|
||||
|
||||
def shot(page, path, full=False):
|
||||
page.screenshot(path=path, full_page=full)
|
||||
print('saved', path)
|
||||
|
||||
|
||||
geo = {}
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(executable_path=CHROME, headless=True,
|
||||
args=['--no-sandbox', '--disable-dev-shm-usage'])
|
||||
# ---------- desktop 1280x900 (pixel-diff set) ----------
|
||||
ctx = browser.new_context(viewport={'width': 1280, 'height': 900}, device_scale_factor=1)
|
||||
page = ctx.new_page()
|
||||
page.goto(BASE, wait_until='networkidle')
|
||||
for lang in ('en', 'fr'):
|
||||
for tag, screen, step in (('quiz-province', 'quiz', 0), ('quiz-vision', 'quiz', 13), ('results', 'results', 13)):
|
||||
set_state(page, screen, step, lang)
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(1600) # charts + vision images settle
|
||||
shot(page, os.path.join(OUTD, f'desktop-{lang}-{tag}.png'))
|
||||
ctx.close()
|
||||
# ---------- mobile 390/360/320 ----------
|
||||
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 lang in ('en', 'fr'):
|
||||
for stag, step in MSTEPS:
|
||||
set_state(page, 'quiz', step, lang)
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(700)
|
||||
shot(page, os.path.join(OUTM, f'{dev}-{lang}-{stag}.png'))
|
||||
geo[f'{dev}:{lang}:{stag}:s0'] = page.evaluate(GEO_JS)
|
||||
if dev == 'iphone14':
|
||||
# province step at scroll 0 (defect shot for FR is above); sheet expand test shots
|
||||
set_state(page, 'quiz', 0, lang)
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(500)
|
||||
try:
|
||||
page.click('.panel-h', timeout=3000)
|
||||
page.wait_for_timeout(450)
|
||||
shot(page, os.path.join(OUTM, f'{dev}-{lang}-sheet-open.png'))
|
||||
geo[f'{dev}:{lang}:sheet-open'] = page.evaluate(GEO_JS)
|
||||
page.click('#scrim', timeout=3000)
|
||||
page.wait_for_timeout(350)
|
||||
geo[f'{dev}:{lang}:sheet-collapsed'] = page.evaluate(
|
||||
'() => ({open: document.querySelector("#panel").classList.contains("open"), scrim: getComputedStyle(document.querySelector("#scrim")).display})')
|
||||
except Exception as e:
|
||||
print('sheet test failed:', lang, e)
|
||||
# intro + results at 390 full page (non-regression of non-quiz screens)
|
||||
set_state(page, 'intro', 0, lang)
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(700)
|
||||
shot(page, os.path.join(OUTM, f'{dev}-{lang}-intro.png'), full=True)
|
||||
geo[f'{dev}:{lang}:intro'] = page.evaluate('() => ({docSH: document.documentElement.scrollHeight, vh: innerHeight})')
|
||||
set_state(page, 'results', 13, lang)
|
||||
page.reload(wait_until='networkidle')
|
||||
page.wait_for_timeout(1600)
|
||||
shot(page, os.path.join(OUTM, f'{dev}-{lang}-results.png'), full=True)
|
||||
geo[f'{dev}:{lang}:results'] = page.evaluate('() => ({docSH: document.documentElement.scrollHeight, vh: innerHeight})')
|
||||
ctx.close()
|
||||
browser.close()
|
||||
|
||||
with open(os.path.join(OUTM, f'geo-{TAG}.json'), 'w') as f:
|
||||
json.dump(geo, f, indent=1, ensure_ascii=False)
|
||||
print('captured', len(geo), 'measurement points ->', os.path.join(OUTM, f'geo-{TAG}.json'))
|
||||
print('CAPTURE-OK')
|
||||
Reference in new issue
Block a user