W testach UI najwięcej kłopotów sprawia nie sam XPath, tylko dobór selektora, który przetrwa zmianę układu, kolejności klas i drobne przebudowy frontu. Technika xpath class contains przydaje się wtedy, gdy trzeba złapać element po fragmencie nazwy klasy, ale nie chce się uzależniać testu od pełnego, kruchego atrybutu. Poniżej pokazuję, kiedy to działa dobrze, gdzie zaczyna się ryzyko i jak pisać selektory, które naprawdę pomagają w automatyzacji.
Najważniejsze zasady dopasowania po klasie w XPath
- `contains()` sprawdza zwykły fragment tekstu, więc `@class` jest traktowany jak jeden napis, a nie lista klas.
- Proste `contains(@class, 'btn')` bywa wygodne, ale może dopasować za dużo elementów.
- Jeśli chcesz trafić w konkretną klasę jako osobny token, użyj wzorca z `concat(' ', normalize-space(@class), ' ')`.
- W Selenium XPath jest elastyczny, ale CSS często bywa czytelniejszy i łatwiejszy w utrzymaniu.
- Najstabilniejsze testy zwykle łączą klasę z innym sygnałem, na przykład `data-testid`, tekstem albo kontekstem w DOM.
Jak działa dopasowanie po klasie w XPath
W specyfikacji XPath funkcja contains() jest banalna: zwraca prawdę, jeśli pierwszy ciąg zawiera drugi. To ważne, bo atrybut class w HTML nie jest pojedynczą etykietą, tylko listą tokenów rozdzielonych spacjami. Dla przeglądarki i XPath to wciąż zwykły tekst, więc wyrażenie contains(@class, 'card') nie szuka „klasy card” w sensie semantycznym, tylko dowolnego fragmentu napisu.
//div[contains(@class, 'card')]
To podejście działa dobrze, gdy nazwy klas są przewidywalne i unikalne, na przykład product-card, promo-card albo user-card. Problem zaczyna się wtedy, gdy szukany fragment jest zbyt krótki. Jeśli wpiszesz contains(@class, 'btn'), złapiesz nie tylko btn, ale też button, btn-primary i część klas pomocniczych. W testach automatycznych takie dopasowanie potrafi przejść lokalnie, a potem rozpaść się na innej wersji frontu. To prowadzi nas do bezpieczniejszego wzorca, który rozróżnia fragment od pełnego tokenu.

Najbezpieczniejszy zapis dla elementów z wieloma klasami
Jeśli chcesz dopasować konkretną klasę, a nie tylko fragment napisu, sam contains() zwykle nie wystarcza. W praktyce stosuję wzorzec z normalize-space() i sztucznymi spacjami po obu stronach. Dzięki temu XPath widzi granice tokenów i nie myli btn z btn-primary albo toolbar-btn.
//button[contains(concat(' ', normalize-space(@class), ' '), ' btn-primary ')]
Ten zapis wygląda dłużej, ale jest znacznie stabilniejszy. normalize-space() usuwa nadmiarowe spacje, a concat(' ', ... , ' ') dodaje kontrolowane granice po obu stronach. W efekcie szukasz pełnej klasy jako osobnego tokenu, a nie przypadkowego fragmentu. To szczególnie przydatne przy komponentach z wieloma klasami, na przykład w Bootstrapie, Tailwindzie albo w aplikacjach, gdzie framework frontowy dokleja swoje modyfikatory do klasy bazowej.
//div[contains(concat(' ', normalize-space(@class), ' '), ' alert ') and contains(., 'Błąd')]
W testach lubię też łączyć warunki. Sama klasa mówi wtedy tylko, że patrzę na właściwy komponent, a tekst, atrybut albo kontekst DOM zawężają wynik do konkretnego przypadku. To ważne, bo selektor oparty wyłącznie na klasie bywa zbyt szeroki, zwłaszcza w dużych panelach administracyjnych i sklepach internetowych. Jeśli dalej potrzebujesz wybrać metodę locatora, dobrze jest zestawić XPath z CSS i prostszymi strategiami Selenium.
Kiedy XPath ma sens, a kiedy lepiej wybrać CSS
Selenium dopuszcza kilka strategii lokalizowania elementów, ale nie każda jest równie wygodna w każdej sytuacji. XPath daje dużą elastyczność, jednak sama dokumentacja Selenium uczciwie zwraca uwagę, że jego składnia jest bardziej złożona i bywa trudniejsza w debugowaniu niż CSS. Z kolei selektor klasy wprost z poziomu By.className nie obsługuje złożonych nazw klas, więc przy kilku tokenach w atrybucie szybko kończy się na obejściu problemu.
| Metoda | Kiedy użyć | Ograniczenie |
|---|---|---|
xpath + contains(@class, ...) |
Gdy klasa jest częściowo przewidywalna, a element trzeba doprecyzować innym warunkiem | Łatwo dopasować za dużo elementów, jeśli fragment jest zbyt krótki |
xpath + normalize-space() |
Gdy chcesz trafić w konkretny token klasy, mimo że element ma wiele klas | Składnia jest dłuższa i mniej „lekka” niż prosty selektor CSS |
By.className |
Gdy szukasz jednej, prostej i stabilnej klasy | Nie nadaje się do złożonych klas i nie rozwiązuje problemu częściowego dopasowania |
| CSS selector | Gdy selektor ma być krótki, czytelny i wystarczająco precyzyjny | Nie ma takiej swobody jak XPath przy łączeniu złożonych warunków i osi DOM |
Ja zwykle wybieram XPath wtedy, gdy muszę zejść głębiej w strukturę DOM albo połączyć kilka warunków naraz. Jeśli jednak wystarcza prosty, stabilny selektor CSS, nie komplikuję testu na siłę. To oszczędza czas przy debugowaniu i zmniejsza ryzyko, że ktoś po tygodniu nie zrozumie, dlaczego dany locator wygląda tak, a nie inaczej. Skoro już widać różnice między metodami, warto przejść do błędów, które najczęściej psują selektory po klasie.
Najczęstsze błędy, które psują selektory w testach
W praktyce większość problemów nie wynika z samego XPath, tylko z tego, że selektor jest za szeroki, za krótki albo oparty na niestabilnym fragmencie frontu. Poniżej zbieram błędy, które widuję najczęściej w automatyzacji testów.-
Zbyt ogólne dopasowanie -
contains(@class, 'item')może trafić w pół aplikacji, jeśli masz klasy typumenu-item,item-cardilist-item. -
Zakładanie, że pełna wartość klasy jest stała -
@class='btn primary'działa tylko wtedy, gdy kolejność i liczba klas się nie zmieni. -
Pomijanie spacji między tokenami - bez
normalize-space()i granic słownych łatwo o fałszywe trafienia. - Używanie klasy z bundlera jako głównego sygnału - klasy generowane przez CSS Modules, build tooling albo obfuskację potrafią zmienić się po wdrożeniu bez żadnej zmiany logiki.
- Brak dodatkowego zawężenia - selektor oparty tylko na klasie jest wygodny, ale w dużym DOM-ie często zwraca zbyt wiele elementów.
- Mieszanie logiki testowej z detalami wizualnymi - jeśli klasa opisuje wygląd, a nie rolę komponentu, to test staje się zależny od refaktoru UI.
Najlepsza obrona jest prosta: zanim zatwierdzę locator, zadaję sobie pytanie, czy ten sam selektor nadal zadziała po dodaniu jednej klasy pomocniczej albo po przemalowaniu komponentu. Jeśli odpowiedź brzmi „nie”, to znaczy, że trzeba go jeszcze doprecyzować lub oprzeć na czymś trwalszym. I właśnie dlatego w dojrzałych projektach warto mieć gotowe wzorce, które da się stosować bez każdorazowego wymyślania koła na nowo.
Gdy klasa przestaje wystarczać, lepszy jest sygnał testowy niż sprytne obejście
W dobrze utrzymanej automatyzacji testów klasa jest tylko jednym z sygnałów, a nie jedynym fundamentem. Jeśli mogę, wybieram stabilniejszy atrybut, na przykład data-testid, albo łączę klasę z kontekstem komponentu. W aplikacjach rozbudowanych to daje dużo lepszy efekt niż próba „uratowania” każdego selektora samym contains().
- Najpierw sprawdzam, czy istnieje stabilny atrybut techniczny, na przykład
data-testidalboaria-label. - Jeśli go nie ma, używam klasy, ale w wersji token-safe, a nie na ślepy substring.
- Gdy komponent występuje wielokrotnie, dodaję kontekst DOM, na przykład sekcję, kartę produktu albo panel formularza.
- Jeśli element ma tekst, który rzeczywiście jest częścią interfejsu, łączę warunek klasy z tekstem, ale tylko wtedy, gdy tekst nie jest podatny na tłumaczenia albo częste zmiany copy.
//section[contains(@class, 'product-card')]//button[contains(@class, 'buy')]
Ten ostatni wzorzec dobrze pokazuje filozofię, którą sam wolę stosować: klasa wskazuje obszar, a struktura DOM albo atrybut techniczny dopina precyzję. Dzięki temu test jest nadal zwięzły, ale nie opiera się na jednym kruchym elemencie. Jeśli więc masz dziś w projekcie selektory budowane wyłącznie na fragmencie klasy, to najrozsądniejszy krok nie polega na dodaniu jeszcze jednego contains(), tylko na znalezieniu stabilniejszego sygnału dla testu.