One CSS line: .lang-toggle{order:99;margin-left:auto}. Spec's literal order:99 alone
failed its own <=2px right-edge acceptance (gap 45.6-467.6px, 18/24 cases — order
reorders, .hdr-in is left-packed; geo-literal-order99.json), so margin-left:auto on
the toggle absorbs the free space; pill/logo untouched (spec-forbidden zone respected).
Battery: A1 gap=0px + rightmost + nothing-past-left in 24/24 (320/360/390/1280 x EN/FR
x intro/quiz/results); A2 header height unchanged 56/66px; A3 320px no overflow/wrap,
FR logo ellipsis intact, elementFromPoint hit-tests lang buttons + Start over pass;
A4 FR/EN clicks switch lang+title+aria-pressed; A5 24 screenshots before/after; A6
engine 36 + i18n 48 + smoke 92 + ps-pension 70 green; A7 desktop pixdiff header-strip-
only, 0 px below header at every device, self-jitter bit-identical; A8 DEPLOY-OK,
origin marker=1 + byte-identical CSS, off-LAN troublebox 200+marker+rule OFFLAN-OK.
68 lines
2.7 KiB
Python
68 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Desktop pixel-diff for the header lang-toggle move (spec acceptance 7).
|
|
|
|
Compares two screenshot dirs (intro/results, EN+FR) and proves the ONLY change is
|
|
inside the header strip: the diff bbox must sit entirely above `hdr_bottom` (masked),
|
|
and everything below the header must be bit-identical. Prints a self-jitter baseline
|
|
DIR_A vs DIR_A2 (same version captured twice) for the jitter-controlled comparison.
|
|
|
|
Usage: python3 audit/lang_right_pixdiff.py BEFORE AFTER HDR_BOTTOM
|
|
python3 audit/lang_right_pixdiff.py SELFJITTER DIR DIR2 HDR_BOTTOM
|
|
"""
|
|
import os, sys
|
|
from PIL import Image, ImageChops
|
|
|
|
# Usage: pixdiff.py BEFORE AFTER HDR_BOTTOM [PREFIX]
|
|
# pixdiff.py SELFJITTER DIR DIR2 HDR_BOTTOM [PREFIX]
|
|
if sys.argv[1] == 'SELFJITTER':
|
|
cross = False
|
|
A, B, HB = sys.argv[2], sys.argv[3], int(sys.argv[4])
|
|
PREFIX = sys.argv[5] if len(sys.argv) > 5 else None
|
|
else:
|
|
cross = True
|
|
A, B, HB = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
PREFIX = sys.argv[4] if len(sys.argv) > 4 else None
|
|
|
|
fa = sorted(f for f in os.listdir(A) if f.endswith('.png') and (PREFIX is None or f.startswith(PREFIX)))
|
|
zero, all_in_hdr = True, True
|
|
for f in fa:
|
|
p = os.path.join(A, f)
|
|
q = os.path.join(B, f)
|
|
if not os.path.exists(q):
|
|
if cross:
|
|
print(f'{f}: MISSING in {B}'); zero = False; all_in_hdr = False
|
|
else:
|
|
print(f'{f}: (no self-jitter pair, skipping)')
|
|
continue
|
|
ia, ib = Image.open(p).convert('RGB'), Image.open(q).convert('RGB')
|
|
if ia.size != ib.size:
|
|
print(f'{f}: SIZE DIFF {ia.size} vs {ib.size}'); zero = False; all_in_hdr = False; continue
|
|
W, H = ia.size
|
|
d = ImageChops.difference(ia, ib)
|
|
bbox = d.getbbox()
|
|
if bbox is None:
|
|
print(f'{f}: 0 px (identical)'); continue
|
|
x0, y0, x1, y1 = bbox
|
|
# mask the header strip: crop everything below HB and diff that
|
|
below = ImageChops.difference(ia.crop((0, HB, W, H)), ib.crop((0, HB, W, H)))
|
|
below_bbox = below.getbbox()
|
|
hist = d.convert('L').histogram()
|
|
ndiff = sum(hist[1:])
|
|
below_hist = below.convert('L').histogram()
|
|
ndiff_below = sum(below_hist[1:])
|
|
if ndiff_below > 0:
|
|
print(f'{f}: {ndiff} px total, {ndiff_below} px BELOW header (bbox {bbox}) <-- LEAK')
|
|
zero = False; all_in_hdr = False
|
|
else:
|
|
tag = 'header-only' if cross else 'identical'
|
|
print(f'{f}: {ndiff} px total, 0 px below y={HB}, bbox_top={y0} bbox_bot={y1} [{tag}]')
|
|
if not cross:
|
|
zero = zero and (ndiff == 0)
|
|
print('---')
|
|
if cross:
|
|
print('PIXDIFF-HEADER-ONLY' if all_in_hdr else 'PIXDIFF-LEAK-BELOW-HEADER')
|
|
sys.exit(0 if all_in_hdr else 1)
|
|
else:
|
|
print('SELFJITTER-ZERO' if zero else 'SELFJITTER-NONZERO')
|
|
sys.exit(0 if zero else 1)
|