Going Stellar

Acasă/Metode

Inginerie · Testare web

Testarea unui site la lățime de telefon: de ce mint capturile headless

Chrome headless nu așază o pagină mai îngust de circa 500 de pixeli CSS, deci o captură de 390 de pixeli e un decupaj dintr-o pagină mai lată, nu un telefon. Testăm lățimile de telefon prin emulare de dispozitiv peste Chrome DevTools Protocol și apoi măsurăm pagina în loc să ne uităm la ea. Șase capcane de pe propriul nostru site s-au văzut doar așa.

Actualizat

Răspunsul scurt

O captură headless la lățime de telefon nu e o randare de telefon. Chrome headless, adică Chrome rulat fără fereastră vizibilă, plafonează viewport-ul de layout (lățimea după care CSS așază pagina) la circa 500 de pixeli CSS. Ceri o fereastră de 390 de pixeli și primești o imagine de 390 de pixeli dintr-un layout de 500. Ca să testezi un site la lățime de telefon, folosește emularea de dispozitiv prin Chrome DevTools Protocol (CDP), care setează chiar viewport-ul de layout, apoi măsoară pagina în loc să judeci imaginea.

Decupajul face două feluri de pagube. Conținutul tăiat la margine arată exact ca un overflow orizontal, așa că vânezi un bug care nu există. Iar media query-urile sub 500 de pixeli nu rulează niciodată, adică exact acolo unde stau problemele reale de pe telefon.

Pasul 1: dovedește viewport-ul înainte să crezi imaginea

Încarcă o pagină-sondă care își scrie singură window.innerWidth și lățimea elementului rădăcină ca text vizibil în document, ca cifra să ajungă în captură, nu într-o consolă pe care n-o citește nimeni. La o fereastră de 390, sonda noastră a afișat 500 la ambele. Unealta de redimensionare a unui server de automatizare a browserului pe care îl folosim s-a lovit de aceeași limită: cerută la 1440, a întors 500.

Remediul e emularea, nu dimensiunea ferestrei. În Puppeteer asta înseamnă page.emulate({ viewport: { width: 390, height: 844, deviceScaleFactor: 3, isMobile: true, hasTouch: true }, userAgent }). Emularea setează lățimea de layout, densitatea de pixeli, atingerea și indicatorul de mobil, așa că media query-urile de sub plafon rulează în sfârșit. Rulează din nou sonda după emulare; cifra trebuie să fie cea cerută.

Pasul 2: măsoară, nu te uita

O captură răspunde dacă pixelii arată bine. Cele mai multe defecte de telefon țin însă de ce lipsește: o coloană scoasă din ecran, o imagine neîncărcată, un conținut care n-a devenit vizibil. De aceea scriptul nostru de test încarcă fiecare pagină la fiecare viewport și raportează cifre: overflow-ul orizontal și elementul vinovat, înălțimea paginii în ecrane, țintele de atingere mai mici de 44 de pixeli, imaginile nerandate, erorile JavaScript și răspunsurile cu status 4xx. Pe site-ul nostru, asta înseamnă 12 pagini la 3 viewport-uri.

Două detalii fac măsurătoarea cinstită. Compară lățimea de derulare a documentului cu lățimea viewport-ului cu orice regulă de ascundere a overflow-ului oprită, fiindcă ascunderea scoate bara de derulare, nu overflow-ul. Și numără imaginile după stare, cu indicatorul complete după ce pagina s-a așezat, nu din ochi.

Cele șase capcane măsurate pe site-ul nostru

1. Podeaua headless. Plafonul de 500 de pixeli de mai sus. Orice altă verificare e fără sens până nu-l excluzi.

2. Imagini lazy în overflow: hidden. O bandă derulantă e o pistă mai lată decât containerul ei, mutată lateral cu un transform, în interiorul unui overflow: hidden. Imaginile împinse afară nu intră niciodată în viewport, deci un browser care respectă loading="lazy" pe bună dreptate nu le încarcă. Într-o bandă, 39 din 56 de imagini nu s-au încărcat niciodată și apăreau ca pătrate albe.

3. Animație legată de derulare pornită din opacity 0. Un element animat cu animation-timeline: view() care pornește de la opacity: 0 păstrează starea asta până începe intervalul. Pe o pagină prea scurtă ca să se deruleze, intervalul nu începe niciodată și conținutul rămâne invizibil. Apariția la derulare animă acum doar transform. Poziția poate depinde de derulare; existența, nu.

4. 1fr are un minim de conținut. În CSS grid, 1fr înseamnă minmax(auto, 1fr), iar minimul auto e cel mai lung cuvânt care nu se poate rupe. O adresă de e-mail a scos din ecran o coloană întreagă de subsol, la 390 de pixeli. Remediul e minmax(0, 1fr). O regulă body { overflow-x: hidden } ascundea simptomul; am scos-o, ca următorul bug de felul ăsta să se vadă.

5. Derularea lină în headless. Cu html { scroll-behavior: smooth }, un window.scrollTo din script nu avea efect în headless, deci testele care derulează și apoi măsoară măsurau începutul paginii. În teste, derulează cu behavior: 'instant'.

6. Specificitatea. .nav a are specificitatea (0,1,1) și bate .btn-primary, care are (0,1,0), așa că butonul din antet moștenea culoarea linkurilor. Layoutul era întreg; câștiga pur și simplu regula greșită.

Emularea nu e motorul telefonului

Emularea de dispozitiv schimbă viewport-ul și user agent-ul. Nu schimbă motorul care rezolvă CSS-ul. Pe pagina unui magazin făcută cu un editor vizual de pagini, titlurile produselor intrau peste insignă și peste preț doar pe iPhone. Chrome pe desktop cu emulare de mobil, editorul constructorului de pagini și un web view nativ pe Mac randau corect, deci trei medii spuneau că pagina e în regulă.

Cauza era -webkit-line-clamp: 0, o valoare invalidă. Chrome o tratează tăcut ca lipsă de limită; iOS Safari a aplicat-o literal, a strâns cutia textului la un rând, iar textul a continuat să fie desenat pe mai multe rânduri. N-a găsit-o reproducerea, ci numărătoarea pe pagina live: 42 de blocuri de text aveau limita de mobil nesetată și exact 6 aveau 0, exact cele 6 stricate. Când un defect apare doar pe un dispozitiv pe care nu-l ai, fă histograma valorilor CSS specifice dispozitivului pe pagina live și caută valoarea rară.

Randează în ambele teme și în ambele regimuri de mișcare

Orice verificare vizuală trebuie rulată în fiecare regim pe care îl are pagina: luminos și întunecat, mișcare redusă și normală. O regulă globală de-a noastră, care strângea la zero toate duratele de animație sub mișcare redusă, a strâns și animația care așeza elementele pe un cerc, așa că cine cerea mai puțină mișcare primea toate elementele grămadă într-un punct. Am văzut-o doar pentru că am randat ambele regimuri.

Contrastul temelor cedează la fel. Într-una dintre aplicațiile noastre Android, culorile scrise direct în cod nu urmau tema întunecată: 77 de texte coborau noaptea sub raportul de contrast WCAG AA de 4,5:1, cel mai rău la 1,03:1. Greșeala în oglindă a apărut după reparație: o culoare de text care se inversează între teme, folosită ca fundal sub text alb fix, dădea 1,18:1. Verificarea care le prinde pe amândouă calculează contrastul pentru fiecare pereche text-fundal, în ambele teme.

Unde am greșit

Știam capcana imaginilor lazy, o scriseserăm, și am reintrodus-o într-o a doua bandă, unde a fost prinsă din nou la 8 din 28 de imagini. Faptul că știam n-a prevenit-o; verificarea da. Regula de ascundere a overflow-ului care ascundea bug-ul din subsol era scrisă tot de noi. Iar la bug-ul care apărea doar pe iPhone am pierdut timp reproducându-l în trei medii înainte să numărăm valorile pe pagina live, care au dus de fapt la cauză.

Limite și listă de verificare

Plafonul headless e un detaliu de implementare observat în august 2026, nu un standard; rulează din nou sonda în loc să te bazezi pe cifra noastră. Verificările astea detectează lipsa: o imagine neîncărcată, un element neașezat, o pagină mai lată decât viewport-ul. Nu spun dacă layoutul e bun, iar o verificare acoperă doar ce măsoară. Un instrument de latență de-al nostru cronometra odată 7% dintr-o cerere și raporta toată cererea drept rapidă. Tot trebuie să se uite un om cu un telefon real în mână.

Lista. Dovedește viewport-ul cu o sondă. Emulează dispozitivul prin CDP; nu te baza niciodată pe dimensiunea ferestrei în headless. Măsoară overflow-ul cu ascunderea oprită. Numără imaginile neîncărcate din containerele care derulează lateral. La derulare animă transform, niciodată opacity. Folosește minmax(0, 1fr) unde conținutul poate fi lung. În teste, derulează instant. Randează luminos și întunecat, cu mișcare redusă și normală. Pentru un bug doar de dispozitiv, numără valorile CSS pe pagina live.

Întrebări și răspunsuri

De ce captura din Chrome headless la 390px pare tăiată?

Chrome headless nu așază o pagină mai îngust de circa 500 de pixeli CSS. O fereastră de 390 de pixeli produce o imagine de 390 de pixeli dintr-un layout de 500, deci conținutul de la marginea dreaptă e decupat și arată ca un overflow orizontal. Folosește emularea de dispozitiv prin Chrome DevTools Protocol, de exemplu page.emulate în Puppeteer, și confirmă lățimea cu o sondă care afișează window.innerWidth.

Cum emulez corect un telefon în Puppeteer?

Cheamă page.emulate cu un viewport care setează width, height, deviceScaleFactor, isMobile și hasTouch, plus un user agent de mobil, de exemplu width 390, height 844 și deviceScaleFactor 3. Asta setează chiar viewport-ul de layout, deci rulează și media query-urile de sub 500 de pixeli. Apoi încarcă o pagină-sondă care afișează window.innerWidth, ca să vezi că emularea a prins.

De ce nu apar imaginile lazy într-o bandă derulantă sau un carusel?

O bandă derulantă mută lateral, cu un transform, o pistă mai lată decât containerul, în interiorul unui overflow: hidden. Imaginile din afara containerului nu intră niciodată în viewport, deci browserul pe bună dreptate nu declanșează loading="lazy". Încarcă imaginile acelea imediat și testează numărând, după ce pagina s-a așezat, imaginile cu indicatorul complete fals din containerele care derulează lateral.

De ce e invizibil conținutul când folosesc animation-timeline: view()?

O animație legată de derulare își păstrează starea inițială până începe intervalul. Dacă elementul pornește de la opacity 0 și pagina e prea scurtă ca să se deruleze, intervalul nu începe niciodată și elementul rămâne invizibil. La derulare animă doar transform, ca poziția să depindă de derulare, dar vizibilitatea nu, și testează atât cu, cât și fără mișcare redusă.

Arată emularea mobilă din Chrome bug-urile din iOS Safari?

Nu. Emularea schimbă viewport-ul și user agent-ul, dar nu și motorul care rezolvă CSS-ul. O valoare invalidă precum -webkit-line-clamp: 0 e ignorată de Chrome și aplicată literal de iOS Safari. Când un defect apare doar pe un dispozitiv pe care nu-l ai, compară valorile CSS specifice dispozitivului pe toată pagina live și caută valoarea rară.

Cum se citează

Going Stellar (2026). Testarea unui site la lățime de telefon: de ce mint capturile headless. GOING STELLAR S.R.L..
https://goingstellar.ro/ro/metode/testare-site-latime-telefon-capturi-headless/

Citește ca Markdown

Toate metodele

Contact

Spune-ne ce nu merge

Scrie în română sau în engleză. Citește un om — de obicei chiar cel care ar face treaba — și răspunde într-o zi lucrătoare.