Testuj piksele: kontrola jakości na zrzutach ekranu w małym zespole
Nasze testy świeciły na zielono, a rozwijanego menu nie dało się kliknąć. Testy jednostkowe nie widzą błędów nakładania, rozsypanego trybu ciemnego ani przepełnienia na telefonie. Oto mała maszynka do zrzutów ekranu, która ogląda teraz za nas każdą powierzchnię, i wizualne błędy, których testy nigdy nie złapią.
Przyjęliśmy to uczciwą drogą: po wypuszczeniu wizualnych regresji, które złapałby jeden zrzut ekranu. Matowy panel, który na ciemnym tle wyszedł blady. Menu, które się renderowało, ale nie dawało się kliknąć. Pole wyszukiwania, które przy szerokości telefonu istniało jako 65-pikselowy pasek. Każde z nich przepłynęło przez zieloną paczkę testów, bo paczka testowała funkcje, a błędy mieszkały w pikselach.
Ta maszynka jest mniejsza, niż myślisz
Nasza to zasiane lokalne środowisko pełne przekonujących wymyślonych danych, jedno logowanie w trybie headless, którego sesja jest zapisana na dysk, żeby nic nie dławiło ruchu, i pętla: strony razy motywy razy rozmiary okna. Komputer i telefon, jasny i ciemny, każda powierzchnia, która ma znaczenie. Całość to krótki skrypt, a pełne przejście produkuje folder obrazów w kilka minut.
import { chromium } from 'playwright'; const browser = await chromium.launch();const ctx = await browser.newContext({ storageState: 'auth.json', // login once, reuse everywhere viewport: { width: 1440, height: 900 },});const page = await ctx.newPage(); for (const [name, url] of PAGES) { await page.goto(BASE + url, { waitUntil: 'load' }); for (const theme of ['light', 'dark']) { await page.evaluate((t) => document.documentElement.setAttribute('data-theme', t), theme); await page.screenshot({ path: `shots/${name}-${theme}.png`, fullPage: true }); }}Zrzuty plus dowód
Obrazy łapią to, co łapią oczy. Przy błędach interakcji chcesz, żeby zeznawała przeglądarka. Dwie jednolinijkowe rzeczy robią większość roboty: elementFromPoint w środku kontrolki mówi, co naprawdę odebrałoby kliknięcie, a getBoundingClientRect mówi, czy kontrolka faktycznie mieści się w oknie, które rzekomo obsługuje. Sprawdzamy teraz oba dla wszystkiego, co nas kiedyś sparzyło.
// Is the menu item really clickable, or is something invisible covering it?const hit = await page.evaluate(() => { const el = document.querySelector('[role="menuitem"]'); const r = el.getBoundingClientRect(); const top = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2); return el === top || el.contains(top);}); // Does the search input actually fit a 390px phone?const fits = await page.evaluate(() => { const r = document.querySelector('input[name="q"]').getBoundingClientRect(); return r.right <= window.innerWidth && r.width > 200;});Oba motywy, za każdym razem
Błędy trybu ciemnego to osobna kategoria, bo ukrywają się przed programistami pracującymi w jednym motywie. Zaszyte na sztywno prawie białe wypełnienie wygląda na zamierzone na jasnej stronie i promieniotwórczo na ciemnej. Przełączenie atrybutu motywu w tej samej wizycie na stronie kosztuje jedną linię i podwaja pokrycie. Każdą powierzchnię, która nie została sfotografowana w obu motywach, traktujemy jako nieprzetestowaną.


Co to łapie
- Błędy kontekstu nakładania: wyrenderowane, widoczne i nieklikalne. Widzi je tylko test trafienia.
- Przecieki motywu: jasne powierzchnie świecące na ciemnych stronach, znikające wykresy, ilustracje z wypalonym tłem.
- Przepełnienie na telefonie: przypięte paski zasłaniające treść, pola przycięte do paseczków, nagłówki ucięte w połowie zdania.
- Tekst w kontekście: ucięcie, sprzeczność, zdanie, którego żaden linter nigdy nie oznaczy.
Nic z tego nie zastępuje testów jednostkowych. Testy dowodzą, że matematyka pieniędzy się zgadza, a zrzuty dowodzą, że człowiek dosięgnie przycisku, który ją uruchamia. Mały zespół nie ma działu kontroli jakości, ale folder świeżych zrzutów przeglądany przed każdym wydaniem jest jego przyzwoitą imitacją. Testem jest patrzenie.
Testy przechodzą to zdanie o Twoich funkcjach. Zrzut ekranu to zdanie o Twoim produkcie.