| 38 |
38 |
const mv = document.querySelector('meta[name="viewport"]'); |
| 39 |
39 |
res.viewport = mv ? mv.getAttribute("content") : null; |
| 40 |
40 |
|
| 41 |
|
− // débordement horizontal + fautifs |
|
41 |
+ // débordement horizontal RÉEL (scrollWidth peut mentir quand overflow-x:clip |
|
42 |
+ // est posé — on vérifie que le viewport défile vraiment) |
| 42 |
43 |
const iw = window.innerWidth; |
| 43 |
|
− res.overflowPx = Math.max( |
|
44 |
+ const rawOverflow = Math.max( |
| 44 |
45 |
document.documentElement.scrollWidth - iw, |
| 45 |
46 |
document.body ? document.body.scrollWidth - iw : 0); |
|
47 |
+ res.overflowPx = 0; |
|
48 |
+ if (rawOverflow > 1) { |
|
49 |
+ const x0 = window.scrollX; |
|
50 |
+ window.scrollBy(rawOverflow, 0); |
|
51 |
+ if (window.scrollX - x0 > 1) res.overflowPx = rawOverflow; |
|
52 |
+ window.scrollTo(x0, 0); |
|
53 |
+ } |
| 46 |
54 |
res.overflowEls = []; |
| 47 |
55 |
if (res.overflowPx > 1) { |
| 48 |
56 |
for (const el of document.querySelectorAll("body *")) { |
| 105 |
113 |
const bodyPad = parseFloat(getComputedStyle(document.body).paddingBottom) || 0; |
| 106 |
114 |
const main = document.querySelector("main"); |
| 107 |
115 |
const mainPad = main ? parseFloat(getComputedStyle(main).paddingBottom) || 0 : 0; |
| 108 |
|
− res.bottomBar = { sel: bar.sel, h: bar.rect.h, bodyPadBottom: bodyPad, mainPadBottom: mainPad }; |
|
116 |
+ let wrapPad = 0; |
|
117 |
+ for (const w of document.querySelectorAll("#root, #__next, .app, #app")) { |
|
118 |
+ wrapPad = Math.max(wrapPad, parseFloat(getComputedStyle(w).paddingBottom) || 0); |
|
119 |
+ } |
|
120 |
+ res.bottomBar = { sel: bar.sel, h: bar.rect.h, bodyPadBottom: bodyPad, mainPadBottom: mainPad, wrapPadBottom: wrapPad }; |
| 109 |
121 |
} |
| 110 |
122 |
|
| 111 |
123 |
// champs zoom iOS |
| 120 |
132 |
if (res.smallInputs.length >= 10) break; |
| 121 |
133 |
} |
| 122 |
134 |
|
| 123 |
|
− // cibles tactiles dans header/nav |
|
135 |
+ // cibles tactiles dans header/nav — en tenant compte des extensions de zone |
|
136 |
+ // par pseudo-élément (::after/::before absolu avec inset négatif) |
|
137 |
+ const hitExt = (el) => { |
|
138 |
+ let ext = 0; |
|
139 |
+ for (const ps of ["::after", "::before"]) { |
|
140 |
+ const cs = getComputedStyle(el, ps); |
|
141 |
+ if (cs.content !== "none" && cs.position === "absolute") { |
|
142 |
+ const t = parseFloat(cs.top), b = parseFloat(cs.bottom); |
|
143 |
+ if (!isNaN(t) && t < 0) ext = Math.max(ext, -t - (isNaN(b) ? 0 : Math.min(b, 0))); |
|
144 |
+ else if (!isNaN(t) && !isNaN(b)) ext = Math.max(ext, Math.max(0, -t) + Math.max(0, -b)); |
|
145 |
+ } |
|
146 |
+ } |
|
147 |
+ return ext; |
|
148 |
+ }; |
| 124 |
149 |
res.smallTargets = []; |
| 125 |
150 |
for (const el of document.querySelectorAll("header a, header button, nav a, nav button, [class*=menu] a, [class*=menu] button")) { |
| 126 |
151 |
const r = el.getBoundingClientRect(); |
| 127 |
152 |
if (r.width === 0 || r.height === 0) continue; |
| 128 |
|
− if ((r.height < 34 || r.width < 34) && r.top >= 0 && r.top < window.innerHeight) { |
| 129 |
|
− res.smallTargets.push({ sel: sel(el), w: Math.round(r.width), h: Math.round(r.height), text: (el.textContent || "").trim().slice(0, 20) }); |
|
153 |
+ const ext = hitExt(el); |
|
154 |
+ const effH = r.height + 2 * ext, effW = r.width + 2 * ext; |
|
155 |
+ if ((effH < 34 || effW < 34) && r.top >= 0 && r.top < window.innerHeight) { |
|
156 |
+ res.smallTargets.push({ sel: sel(el), w: Math.round(effW), h: Math.round(effH), text: (el.textContent || "").trim().slice(0, 20) }); |
| 130 |
157 |
if (res.smallTargets.length >= 10) break; |
| 131 |
158 |
} |
| 132 |
159 |
} |
| 230 |
257 |
if (d.overflowPx > 1) r.issues.push(`débordement horizontal +${d.overflowPx}px : ${(d.overflowEls || []).map((e) => e.sel).slice(0, 4).join(", ")}`); |
| 231 |
258 |
for (const f of d.fixed || []) if (f.brokenBy) r.issues.push(`fixed cassé par ancêtre ${f.brokenBy} → [${f.sel}]`); |
| 232 |
259 |
const bb = d.bottomBar; |
| 233 |
|
− if (bb && bb.bodyPadBottom < bb.h - 8 && bb.mainPadBottom < bb.h - 8) |
| 234 |
|
− r.issues.push(`pas de compensation padding sous la barre basse [${bb.sel}] h=${bb.h}px (body ${bb.bodyPadBottom}px / main ${bb.mainPadBottom}px)`); |
|
260 |
+ if (bb && !/cookie|consent|banner/i.test(bb.sel) && bb.bodyPadBottom < bb.h - 8 && bb.mainPadBottom < bb.h - 8 && (bb.wrapPadBottom ?? 0) < bb.h - 8) |
|
261 |
+ r.issues.push(`pas de compensation padding sous la barre basse [${bb.sel}] h=${bb.h}px (body ${bb.bodyPadBottom}px / main ${bb.mainPadBottom}px / wrapper ${bb.wrapPadBottom ?? 0}px)`); |
| 235 |
262 |
if (bar && !/env\(|calc\(/.test(bar.cssBottom || "") && r.css.safeArea === 0) |
| 236 |
263 |
r.issues.push(`aucune trace de safe-area-inset-bottom (barre basse [${bar.sel}])`); |
| 237 |
264 |
if (r.css.vh100 > 0 && r.css.dvh === 0 && r.css.svh === 0) r.issues.push(`${r.css.vh100}× 100vh sans dvh/svh`); |
| 239 |
266 |
if (d.smallInputs.length) r.issues.push(`${d.smallInputs.length} champ(s) <16px (zoom iOS) : ${d.smallInputs.slice(0, 3).map((i) => i.sel).join(", ")}`); |
| 240 |
267 |
if (d.smallTargets.length) r.issues.push(`${d.smallTargets.length} cible(s) tactile(s) <34px : ${d.smallTargets.slice(0, 3).map((t) => t.sel + "(" + t.w + "×" + t.h + ")").join(", ")}`); |
| 241 |
268 |
|
| 242 |
|
− // test tap : ouvrir le premier bouton menu/hamburger trouvé |
|
269 |
+ // test tap : ouvrir le VRAI bouton de menu (aria-label/menu-btn/burger en |
|
270 |
+ // priorité — pas n'importe quel bouton du header), vérifier qu'un panneau |
|
271 |
+ // s'ouvre réellement (aria-expanded / classe open / dialog) puis le verrou. |
| 243 |
272 |
try { |
| 244 |
|
− const btn = page.locator('header button, [class*="burger"], [class*="hamburger"], button[aria-label*="menu" i], button[aria-label*="Menu"]').first(); |
|
273 |
+ const btn = page.locator('button[aria-label*="menu" i], .menu-btn, [class*="burger"], [class*="hamburger"], label[for*="menu"]').first(); |
| 245 |
274 |
if (await btn.count()) { |
| 246 |
|
− const before = await page.evaluate(() => document.body.innerHTML.length); |
| 247 |
275 |
await btn.tap({ timeout: 3000 }).catch(() => btn.click({ timeout: 3000 })); |
| 248 |
276 |
await page.waitForTimeout(600); |
| 249 |
277 |
const after = await page.evaluate(() => ({ |
| 250 |
|
− len: document.body.innerHTML.length, |
| 251 |
|
− bodyOverflow: getComputedStyle(document.body).overflow, |
|
278 |
+ opened: !!document.querySelector('.mobile-menu.open, [role="dialog"], .gk-mobile-overlay, [aria-expanded="true"], .mm-backdrop, input[id*="menu"]:checked'), |
|
279 |
+ bodyOverflow: (document.body.style.overflow || getComputedStyle(document.body).overflow), |
| 252 |
280 |
htmlOverflow: getComputedStyle(document.documentElement).overflow, |
|
281 |
+ htmlLock: document.documentElement.classList.contains("ka-scroll-lock") || document.documentElement.classList.contains("kaa-lock"), |
| 253 |
282 |
})); |
| 254 |
|
− const opened = Math.abs(after.len - before) > 50; |
| 255 |
|
− if (opened && after.bodyOverflow !== "hidden" && after.htmlOverflow !== "hidden") |
|
283 |
+ if (after.opened && !after.htmlLock && |
|
284 |
+ !/hidden/.test(after.bodyOverflow) && !/hidden/.test(after.htmlOverflow)) |
| 256 |
285 |
r.issues.push("menu ouvert SANS blocage du scroll d'arrière-plan"); |
| 257 |
|
− if (shot) await page.screenshot({ path: shot.replace(".png", "-menu.png") }); |
|
286 |
+ if (shot && after.opened) await page.screenshot({ path: shot.replace(".png", "-menu.png") }); |
| 258 |
287 |
await page.keyboard.press("Escape").catch(() => {}); |
| 259 |
288 |
await page.mouse.click(5, Math.round((page.viewportSize()?.height || 800) * 0.75)).catch(() => {}); |
| 260 |
289 |
} |