Gdy element potrzebny w teście nie ma własnego stabilnego identyfikatora, często najłatwiej znaleźć go przez relację z innym elementem. Technika xpath parent pozwala przejść od znanego węzła do jego rodzica, kontenera formularza, wiersza tabeli albo przycisku akcji. Pokażę, jak działają osie parent i ancestor, kiedy używać skrótu .. oraz jak budować lokatory odporne na zmiany interfejsu.
Najważniejsze zasady wyboru elementu nadrzędnego
-
parent::wybiera bezpośredniego rodzica aktualnego węzła. -
..jest krótkim zapisem przejścia do rodzica. -
ancestor::pozwala znaleźć dowolnego przodka, także kilka poziomów wyżej. -
Relatywny XPath jest zwykle stabilniejszy niż ścieżka zaczynająca się od
/html. -
normalize-space()pomaga ignorować przypadkowe spacje w tekście elementu.
Jak działa wybieranie węzła nadrzędnego w XPath
XPath traktuje dokument XML lub HTML jak drzewo. Element, na którym aktualnie pracujemy, jest węzłem kontekstowym, a jego bezpośredni kontener to rodzic. W praktyce oznacza to, że znając tekst etykiety, nazwę pola albo ikonę, mogę przejść wyżej do elementu, który zawiera przycisk, komunikat lub cały wiersz formularza.
Najbardziej czytelna składnia wygląda tak:
//input[@name='email']/parent::div
Wyrażenie najpierw znajduje pole o nazwie email, a później wybiera jego bezpośredniego rodzica typu div. Jeśli rodzic może mieć różny typ elementu, używam bardziej ogólnego wariantu:
//input[@name='email']/parent::*
W wielu przypadkach wystarczy skrót ..:
//input[@name='email']/..
Sam korzystam z niego przy krótkich, lokalnych selektorach. Gdy XPath ma być częścią większego frameworka testowego, częściej wybieram pełne parent::, ponieważ od razu pokazuje intencję lokatora i ułatwia późniejsze utrzymanie kodu.
Parent, ancestor i skrót dwóch kropek
Najwięcej pomyłek bierze się z traktowania tych konstrukcji jako identycznych. Nie są identyczne. parent:: działa tylko na jeden poziom, a ancestor:: przeszukuje wszystkich przodków aż do korzenia dokumentu.
| Wyrażenie | Co wybiera | Kiedy użyć |
|---|---|---|
.. |
Bezpośredniego rodzica | Gdy struktura jest prosta i dobrze znana |
parent::div |
Bezpośredniego rodzica typu div
|
Gdy chcesz ograniczyć wynik do konkretnego elementu |
parent::* |
Bezpośredniego rodzica będącego elementem | Gdy typ rodzica może się zmieniać |
ancestor::section |
Każdą nadrzędną sekcję typu section
|
Gdy potrzebny kontener znajduje się kilka poziomów wyżej |
ancestor-or-self::div |
Bieżący element lub jego przodków typu div
|
Gdy element kontekstowy sam może być właściwym kontenerem |
Załóżmy, że struktura formularza zawiera dodatkowy wrapper:
Wyrażenie //input[@name='email']/parent::div zwróci field-wrapper, a nie cały wiersz formularza. Do elementu form-row trzeba przejść przez przodków:
//input[@name='email']/ancestor::div[contains(@class, 'form-row')]
To rozróżnienie ma praktyczne znaczenie. Gdy użyję parent:: tam, gdzie potrzebne jest kilka poziomów wyżej, test znajdzie poprawny element tylko w jednej wersji DOM-u i zacznie się psuć po dodaniu niewinnego kontenera.
![Schemat wyjaśnia składnię XPath: `//tagname[@Attribute='Value']`. Określa bieżący węzeł, nazwę atrybutu i jego wartość, co jest kluczowe dla selekcji elementów.](https://cdn.qalabs.pl/unsafe/rs:fit:2048/q:75/plain/blog-assets%2Fpost_image%2Ffa651f9f541b146d6d32affbe861dee7%2Fxpath-parent-axis-diagram-selenium-test-automation-dom-tree.webp)
Praktyczne wzorce w automatyzacji testów
Najczęstszy scenariusz polega na znalezieniu elementu po tekście, a potem przejściu do kontrolki znajdującej się w tym samym komponencie. Przykładowo nazwa produktu może być stabilna, ale przycisk usuwania ma wyłącznie klasę generowaną przez bibliotekę interfejsu.
//span[normalize-space()='Klawiatura']/parent::div//button
Takie wyrażenie odnajduje nazwę produktu, przechodzi do bezpośredniego kontenera i szuka w nim przycisku. W Selenium można użyć go na przykład tak:
driver.findElement(
By.xpath("//span[normalize-space()='Klawiatura']/parent::div//button")
).click();
Jeżeli między nazwą a przyciskiem występują dodatkowe warstwy, lepiej odwołać się do konkretnego przodka:
//span[normalize-space()='Klawiatura']
/ancestor::div[contains(@class, 'cart-row')]
//button[contains(@class, 'remove')]
W testach formularzy przydatny jest też wzorzec oparty na etykiecie. Pozwala powiązać tekst widoczny dla użytkownika z polem, nawet gdy atrybut id zmienia się między środowiskami:
//label[normalize-space()='Adres e-mail']
/ancestor::div[contains(@class, 'form-row')]
//input
Używam tego podejścia wtedy, gdy komponent ma wyraźną strukturę i etykieta jest rzeczywiście unikalna. Jeżeli w formularzu występują dwie identyczne etykiety, sam tekst nie wystarczy. Wtedy trzeba dodać atrybut formularza, indeks albo stabilny identyfikator testowy.
W kodzie JavaScript XPath można sprawdzić przez document.evaluate(). To szybki sposób na diagnozę lokatora bez uruchamiania całego testu:
const result = document.evaluate(
"//label[normalize-space()='Adres e-mail']/parent::div",
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
console.log(result.singleNodeValue);
W narzędziach takich jak Selenium wybór rodzica jest szczególnie przydatny przy tabelach, kartach produktów, formularzach i komponentach z powtarzalną strukturą. Nie traktuję go jednak jako zamiennika dobrych atrybutów testowych. Jeśli zespół może dodać data-testid albo stabilny id, zwykle będzie to prostsze i mniej podatne na zmiany układu.
Jak budować stabilne lokatory oparte na rodzicu
Sam fakt, że XPath działa, nie oznacza jeszcze, że nadaje się do długiej eksploatacji. W automatyzacji testów liczy się nie tylko znalezienie elementu dzisiaj, ale także odporność lokatora na zmiany CSS, dodatkowe wrappery i przestawienie kolejności komponentów.
Wybieraj punkty orientacyjne, nie pozycję w DOM
Ścieżka /html/body/div[3]/div[2]/button może przejść lokalnie, ale jest bardzo krucha. Wystarczy dodać baner, modal albo nowy kontener, aby indeksy przestały wskazywać ten sam element. Znacznie lepiej zacząć od unikalnego tekstu, atrybutu lub roli i dopiero potem przejść do rodzica.
//button[@data-testid='save-profile']
Jeśli taki atrybut nie istnieje, można użyć relacji między elementami:
//span[normalize-space()='Zapisz']
/ancestor::form[@aria-label='Profil użytkownika']
//button
Uważaj na klasy i tekst
Klasy CSS bywają generowane automatycznie, dlatego selektor oparty na pełnej wartości @class szybko traci aktualność. Bezpieczniejszy wariant sprawdza, czy klasa należy do listy klas elementu:
//input/ancestor::div[
contains(concat(' ', normalize-space(@class), ' '), ' form-row ')
]
Funkcja normalize-space() usuwa nadmiarowe spacje na początku, na końcu i między fragmentami tekstu. Przy interfejsach tworzonych przez frameworki frontendowe jest to drobiazg, który potrafi uratować test przed pozornie losowym błędem.
Przeczytaj również: Odświeżanie strony w Selenium - Jak uniknąć błędów?
Ogranicz wynik do jednego komponentu
Wyrażenie //button zwraca wszystkie przyciski w dokumencie. Gdy chcę kliknąć akcję dotyczącą konkretnego wiersza, najpierw zawężam zakres do jego rodzica, a dopiero później szukam przycisku wewnątrz.
//tr[td[normalize-space()='Zamówienie 1042']]//button[@aria-label='Edytuj']
Takie podejście poprawia czytelność i ogranicza ryzyko kliknięcia niewłaściwego elementu. Największą różnicę robi nie sama składnia XPath, lecz świadome ustawienie właściwego węzła kontekstowego.
Najczęstsze błędy i szybka diagnostyka
Gdy lokator z rodzicem nie działa, najpierw sprawdzam, jaki element faktycznie jest kontekstem. Częsty błąd polega na założeniu, że widoczny kontener jest bezpośrednim rodzicem, choć w DOM-ie znajduje się między nimi jeszcze jeden wrapper.
-
Użycie
parent::zamiastancestor::powoduje zatrzymanie się o jeden poziom za wcześnie. -
Brak prefiksu
.w XPath względnym może sprawić, że wyszukiwanie zacznie się od całego dokumentu, a nie od bieżącego elementu. -
Niepoprawny indeks często wynika z różnicy między
//div[1]a(//div)[1]. Pierwsze wyrażenie wybiera pierwszy element w każdym pasującym kontekście, drugie pierwszy wynik całego zapytania. -
Przypadkowe spacje w tekście można zwykle obsłużyć przez
normalize-space(). - Iframe i Shadow DOM wymagają osobnego przełączenia kontekstu albo innego mechanizmu dostępu. Sam XPath nie przejdzie automatycznie przez granicę iframe.
- XML z przestrzeniami nazw może wymagać zarejestrowania prefiksu namespace, inaczej pozornie poprawne wyrażenie zwróci pusty wynik.
W przeglądarce testuję XPath bezpośrednio w konsoli, używając $x("..."), jeśli narzędzia deweloperskie daną funkcję udostępniają. Sprawdzam też liczbę wyników. Lokator, który miał zwrócić jeden element, ale znajduje pięć, jest problemem nawet wtedy, gdy test chwilowo przechodzi.
Osobno kontroluję moment wykonania zapytania. Jeżeli komponent jest renderowany asynchronicznie, poprawny XPath może zwrócić pusty wynik, zanim element pojawi się w DOM-ie. W Selenium potrzebne będzie oczekiwanie na obecność lub widoczność elementu, a nie kolejna modyfikacja ścieżki.
Reguła, która pomaga pisać lepsze lokatory
Najprostszy schemat wygląda tak: znajdź stabilny element, przejdź do właściwego rodzica i dopiero w jego obrębie wybierz kontrolkę. Dzięki temu XPath opisuje relację w komponencie, a nie przypadkowy układ całej strony.
W praktyce zaczynam od parent:: lub .., gdy potrzebny jest bezpośredni kontener. Sięgam po ancestor::, gdy struktura ma kilka warstw, a przed zapisaniem finalnego lokatora sprawdzam, czy wynik jest jednoznaczny i czy nie istnieje prostszy atrybut testowy. Taka dyscyplina zwykle daje stabilniejsze testy niż rozbudowywanie długiej ścieżki o kolejne indeksy.