Gdy interfejs aplikacji zmienia klasy CSS albo generuje identyfikatory przy każdym uruchomieniu, test automatyczny łatwo zaczyna trafiać w niewłaściwy element. Funkcja contains() w XPath pomaga budować bardziej elastyczne lokatory, które wyszukują fragment tekstu, wartości atrybutu lub klasy HTML. Pokażę, jak działa, kiedy rzeczywiście zwiększa stabilność testów i w jakich sytuacjach lepiej użyć innego rozwiązania.
Najważniejsze zasady pracy z contains()
-
Składnia funkcji to
contains(tekst, szukany_fragment), a wynikiem jest wartość logiczna. - Wielkość liter ma znaczenie, dlatego tekst „Login” i „login” nie są traktowane identycznie.
- Do klas CSS używaj wzorca z spacjami ochronnymi, aby nie dopasować przypadkiem podobnej nazwy.
- Krótki XPath oparty na stabilnym atrybucie zwykle działa lepiej niż długa ścieżka zależna od struktury DOM.
- contains() nie zastępuje asercji i nie gwarantuje, że wybrany zostanie tylko jeden element.
Co naprawdę robi contains() w XPath
Funkcja contains() sprawdza, czy pierwszy argument zawiera drugi jako fragment tekstu. Zwraca true albo false, więc najczęściej używa się jej wewnątrz predykatu, czyli warunku umieszczonego w nawiasach kwadratowych.
contains(tekst, szukany_fragment)
Przykładowy selektor wybierający przycisk, którego tekst zawiera słowo „Zaloguj”, wygląda tak:
//button[contains(., 'Zaloguj')]
Kropka oznacza tutaj tekst aktualnego węzła. Dzięki temu lokator może znaleźć zarówno przycisk z napisem „Zaloguj”, jak i „Zaloguj się teraz”. To wygodne, ale ma też konsekwencję: częściowe dopasowanie może wskazać więcej elementów, niż zakładaliśmy.
Funkcja działa również na atrybutach. Jeśli aplikacja generuje identyfikatory w rodzaju user-48392, można odwołać się do stałego fragmentu:
//*[@id and contains(@id, 'user-')]
W praktyce często nie trzeba dodawać warunku @id, bo samo użycie atrybutu wystarczy:
//*[contains(@id, 'user-')]
Trzeba pamiętać, że porównanie jest czułe na wielkość liter. XPath 1.0 nie oferuje prostego przełącznika ignorowania wielkości liter, dlatego przy danych o nieprzewidywalnym zapisie stosuje się między innymi funkcję translate(). W testach UI lepiej jednak najpierw znaleźć stabilny atrybut lub tekst, zamiast komplikować lokator.
Najpraktyczniejsze selektory dla automatyzacji testów
Największą wartość funkcja daje tam, gdzie część wartości jest zmienna, a część pozostaje stała. Dotyczy to dynamicznych identyfikatorów, nazw klas, komunikatów oraz tekstów przycisków.
Fragment tekstu elementu
//a[contains(., 'Szczegóły')]
Ten zapis pasuje do linków zawierających wskazane słowo. Używam go chętnie, gdy tekst może zostać rozszerzony na przykład z „Szczegóły” do „Zobacz szczegóły zamówienia”. Jeśli jednak na stronie występuje kilka podobnych linków, dodaję kontekst rodzica:
//tr[.//td[contains(., 'Zamówienie 123')]]//a[contains(., 'Szczegóły')]
Selektor najpierw znajduje wiersz tabeli z właściwym zamówieniem, a dopiero potem link w tym wierszu. To ważna różnica. Samo szukanie tekstu na całej stronie może przejść przypadkiem, dopóki interfejs nie zyska drugiego podobnego elementu.
Fragment wartości atrybutu
//input[contains(@name, 'email')]
Takie wyrażenie pasuje do pól o nazwach email, billing-email czy user-email-confirmation. Jeżeli aplikacja ma kilka takich pól, warto ograniczyć wybór do formularza albo konkretnego kontenera.
//form[@id='register-form']//input[contains(@name, 'email')]
Przeczytaj również: Selenium 4 - Czy Twoje testy są gotowe na rewolucję?
Klasa CSS bez przypadkowych dopasowań
Prosty zapis poniżej wygląda niewinnie, ale może zwrócić element z klasą btn-primary-large, choć szukaliśmy klasy btn-primary:
//*[contains(@class, 'btn-primary')]
Bezpieczniejszy wzorzec otacza wartość spacjami. Funkcja concat() tworzy tekst, w którym każda nazwa klasy jest traktowana jako osobny token:
//*[contains(concat(' ', normalize-space(@class), ' '), ' btn-primary ')]
To jeden z tych zapisów, które na początku wydają się przesadnie długie, ale w większym projekcie szybko się zwracają. Unikają fałszywych dopasowań, zwłaszcza gdy nazwy klas mają wspólne prefiksy.

Jak używać contains() w Selenium
W Selenium wyrażenie XPath przekazuje się jako drugi argument do metody lokalizującej element. Przykład w Pythonie może wyglądać tak:
from selenium.webdriver.common.by import By
login_button = driver.find_element(
By.XPATH,
"//button[contains(., 'Zaloguj')]"
)
login_button.click()
W tym przypadku test nie zależy od pełnego tekstu przycisku. To przydatne, gdy aplikacja zmienia dopisek, ale podstawowa etykieta pozostaje taka sama. Sam stosuję tę technikę głównie dla tekstów kontrolowanych przez produkt, a nie dla komunikatów, które mogą być tłumaczone lub często redagowane.
Można też pobrać wszystkie pasujące elementy:
buttons = driver.find_elements(
By.XPATH,
"//button[contains(@class, 'primary')]"
)
Metoda find_elements() zwróci pustą listę, jeśli nic nie pasuje, albo wiele elementów, gdy warunek jest zbyt szeroki. Dlatego po znalezieniu elementu dobrze sprawdzić, czy liczba wyników odpowiada założeniu:
buttons = driver.find_elements(
By.XPATH,
"//button[contains(., 'Zapisz')]"
)
assert len(buttons) == 1
Sam lokator nie powinien pełnić roli pełnej walidacji interfejsu. Jego zadaniem jest wskazanie elementu, natomiast asercja potwierdza, że strona zachowuje oczekiwany stan.
Typowe błędy i ograniczenia częściowego dopasowania
Najczęstszy problem nie wynika z błędnej składni, tylko z tego, że warunek jest zbyt ogólny. XPath działa poprawnie, lecz wybiera więcej elementów albo element niewłaściwego rodzaju.
| Problem | Ryzyko | Lepsze podejście |
|---|---|---|
contains(@class, 'card') |
Dopasowanie klas discount-card lub card-large
|
Użycie wzorca z normalize-space() i spacjami |
contains(., 'Usuń') |
Wybór kilku przycisków usuwania | Dodanie kontekstu wiersza, formularza lub identyfikatora |
contains(@id, 'item') |
Zależność od szczegółu, który może zostać zmieniony | Wybór stabilnego atrybutu testowego, jeśli jest dostępny |
| Tekst o innej wielkości liter | Brak dopasowania mimo podobnego wyglądu | Ujednolicenie danych albo świadome użycie translate()
|
Drugie nieporozumienie dotyczy białych znaków. Tekst elementu może zawierać przejście do nowej linii lub dodatkowe spacje, dlatego dokładne porównanie text()='Zaloguj' bywa kruche. W takich przypadkach przydaje się:
//button[contains(normalize-space(.), 'Zaloguj')]
normalize-space() usuwa zbędne spacje na początku i końcu oraz zamienia ich ciągi na pojedynczą spację. Nie rozwiązuje każdego problemu z formatowaniem, ale często eliminuje awarie wynikające wyłącznie z układu tekstu w HTML.
Kiedy wybrać contains(), a kiedy inną metodę
Nie traktuję tej funkcji jako domyślnego rozwiązania dla każdego lokatora. Jeśli element ma stabilny identyfikator albo specjalny atrybut testowy, dokładne dopasowanie jest zwykle prostsze i bardziej jednoznaczne.
| Potrzeba | Przykład | Kiedy ma sens |
|---|---|---|
| Dokładna wartość | //button[@data-testid='save-order'] |
Gdy atrybut jest stabilny i kontrolowany przez zespół |
| Fragment wartości | //button[contains(@data-testid, 'save-')] |
Gdy część identyfikatora jest stała, a reszta dynamiczna |
| Początek wartości | //input[starts-with(@id, 'user-')] |
Gdy ważny jest prefiks, a nie dowolne wystąpienie tekstu |
| Selektor CSS | button[data-testid='save-order'] |
Gdy prosty selektor CSS wystarcza i nie trzeba przechodzić po tekście |
W testach przeglądarkowych XPath daje dużą elastyczność, lecz bywa trudniejszy w debugowaniu niż CSS. Dlatego wybieram go wtedy, gdy potrzebuję relacji między elementami, wyszukiwania po tekście albo warunku, którego CSS nie wyraża równie czytelnie.
Dobrym kompromisem jest dodanie do aplikacji stabilnego atrybutu, takiego jak data-testid. Wtedy lokator nie musi analizować klas generowanych przez framework ani zależeć od tekstu widocznego dla użytkownika.
Jak budować stabilne wyrażenia XPath
Najpierw szukam elementu, który ma najbardziej trwałą cechę. Może to być atrybut testowy, nazwa pola albo kontekst konkretnego formularza. Dopiero gdy wartość jest częściowo zmienna, dokładam contains().
- Sprawdź, czy istnieje stabilny
id,namealbodata-testid. - Jeśli wartość jest dynamiczna, wybierz stały fragment i ogranicz zakres wyszukiwania.
- Przy klasach używaj dopasowania całego tokenu, a nie przypadkowego fragmentu.
- Zweryfikuj, ile elementów zwraca lokator.
- Przetestuj go także po zmianie tekstu, kolejności elementów i rozmiaru danych.
Unikam długich ścieżek w rodzaju /html/body/div[2]/div[1]/section/div[3]/button. Taki XPath może działać dziś, ale jest mocno związany z aktualnym DOM-em. Krótszy zapis oparty na stabilnym atrybucie i rozsądnym warunku zwykle lepiej znosi rozwój interfejsu.
Warto też sprawdzić, czy lokator nie pasuje do elementu ukrytego. contains() filtruje węzły na podstawie tekstu lub atrybutów, ale sama funkcja nie wie, czy element jest widoczny, aktywny albo możliwy do kliknięcia. To powinien zweryfikować test lub mechanizm oczekiwania Selenium.
Od elastycznego dopasowania do pewnego testu
contains() jest bardzo dobrym narzędziem do pracy z dynamicznym HTML-em, ale nie zastąpi projektowania stabilnych selektorów. Najlepsze rezultaty daje połączenie częściowego dopasowania z kontekstem rodzica, normalizacją tekstu i kontrolą liczby znalezionych elementów.
Moja praktyczna zasada jest prosta: używam tej funkcji, gdy naprawdę zmienna jest tylko część wartości. Jeżeli mogę oprzeć test na jednoznacznym data-testid lub dokładnym identyfikatorze, wybieram prostszy lokator, bo każdy dodatkowy warunek zwiększa koszt późniejszego debugowania.
Tak zbudowane wyrażenia są czytelniejsze, mniej podatne na przypadkowe dopasowania i znacznie lepiej sprawdzają się w długowiecznych testach automatycznych.