Lokalizowanie elementów w Selenium – selektory CSS

Selektory CSS to chyba moje ulubione selektory, których używam w testach Selenium. Cenię je sobie za spore możliwości ale też wysoką czytelność i intuicyjność. W poprzednich wpisach z cyklu Selektory w Selenium, opisałam zarówno podstawowe metody używane w Selenium jak i pokazałam trudne przypadki w których najprostsze metody zwyczajnie nie dawały rady. Tam też znajdziesz informację, w jakich sytuacjach selektory CSS i XPath mają przewagę nad pozostałymi. W tym wpisie dokładniej omówię możliwości selektorów CSS oraz wyjaśnię (jak zwykle na przykładach) czym to jest. Dla uproszczenia użyję jedynie metody FindElement ale możecie w każdym z tych przypadków użyć także FindElements. Selektory CSS, jak wszystkie pozostałe, współpracują z obiema metodami.

Czym jest CSS?

CSS, czyli kaskadowe arkusze stylów, pozwalają zdefiniować style, czyli sposób prezentacji elementów HTML na stronie. Weźmy na warsztat kontener zawierający moduł do interakcji ze stroną na Facebooku ze strony automationpractice.com. Otwórzmy zatem Developer Tools w Chrome (F12), wybierzmy ikonkę inspektora i zaznaczmy element klikając na niego. Zobaczysz coś takiego:

Konsola deweloperska - przykład stylu CSS na stronie automationpractice.com
Gdzie szukać informacji o CSS w konsoli deweloperskiej

Po lewej widzisz HTML z zaznaczonym elementem, po prawej zaś są style. Możesz zobaczyć poszczególne atrybuty, jak np. „background: white”, ale też z jakiego pliku dany styl został załadowany (global.css). Skupmy się na chwilę na tym fragmencie:

#facebook_block .facebook-fanbox{
	background: white;
	border: 1px solid #aaaaaa;
	padding-bottom: 10px;
}

Znak „#” w CSS oznacza id a „.” klasę. Zatem „#facebook_block” oznacza wszystkie elementy o id równym„facebook_block”, a „.facebook-fanbox” wszystkie elementy o klasie „facebook-fanbox”. A taka kombinacja jak powyżej? Spacja pomiędzy oznacza potomka, czyli tłumacząc na ludzkie cały fragment:

Dla wszystkich elementów o klasie „facebook-fanbox”, które są potomkami elementu o id równym „facebook_block”, nadaj poniższe style:
kolor tła: biały (white),
obramowanie: pełne (solid), szerokość jednego piksela w kolorze szarym (podanym w kodzie heksadecymalnym jako #aaaaaa),
przestrzeń poniżej elementu: 10 pikseli szerokości.

Element o klasie „facebook-fanbox” w istocie jest potomkiem elementu o id „facebook_block”:

<div id="facebook_block" class="col-xs-4">
	<h4>Follow us on Facebook</h4>
	<div class="facebook-fanbox">...</div>
</div>

Przez potomka należy rozumieć… potomka. Drugi element nie musi być zatem bezpośrednio „pod” pierwszym. Wystarczy, że jest w hierarchii gdzieś pod nim, czyli jest np. bezpośrednim potomkiem (dzieckiem) elementu, który jest bezpośrednio pod pierwszym elementem. Najłatwiej to zrozumieć na przykładzie:

<div id="facebook_block" class="col-xs-4">
	<h4>Follow us on Facebook</h4>
	<div id="dodatkowy_div">
		<div class="facebook-fanbox">...</div>
	</div>
</div>

W tym wypadku div o klasie facebook-fanbox nadal jest potomkiem. Ale w poniższym przykładzie już nie:

<div id="facebook_block" class="col-xs-4">
	<h4>Follow us on Facebook</h4>
	<div id="dodatkowy_div">...</div>
</div>
<div class="facebook-fanbox">...</div>

Po co mnie tu te CSSy, zapytasz. Selektory chciałem poznać, psiakość, a nie uczyć się web developmentu. A bo widzisz, „#facebook_block .facebook-fanbox” to właśnie selektor CSS. Pliki ze stylami używają go do określenia, które elementy strony mają przyjąć przypisane w nawiasach klamrowych style. My możemy za ich pomocą powiedzieć Web Driverowi, o które elementy nam chodzi.

Teraz mięso. Co możemy zrobić tymi selektorami?

Szukanie elementu po atrybucie

Składnia podstawowego selektora CSS służącego do namierzenia elementu po dokładnej wartości atrybutu w uogólnieniu wygląda tak:

[atrybut='wartość']

Czyli chcąc znaleźć link do bestsellerów („BEST SELLERS”) z poniższego screenshota ze strony automationpractice.com, możemy się posłużyć np. atrybutem „class”.

_driver.FindElement(By.CssSelector("[class='blockbestsellers']"));
Przykład web elementu ze strony automationpractice.com
Przykład web elementu – selektor CSS

Równie dobrze możemy w tym celu wykorzystać wartość parametru „data-toggle” lub parametru „href”, czyli adresu pod który link prowadzi. Albo ich obu na raz – ta opcja przydaje się, gdy do jednoznacznego zidentyfikowania obiektu potrzebujemy użyć jego dwóch atrybutów.

_driver.FindElement(By.CssSelector("[data-toggle='tab']"));
_driver.FindElement(By.CssSelector("[href='#blockbestsellers']"));
_driver.FindElement(By.CssSelector("[data-toggle='tab'][href='#blockbestsellers']"));

Możemy również wyszukiwać elementy jedynie po części nazwy klasy. Służy do tego znak * umieszczony zaraz po nazwie atrybutu a przed znakiem równości. Zatem poniższy selektor także zwróci link do bestsellerów:

_driver.FindElement(By.CssSelector("[class*='bestseller']"));

Istnieje także opcja znajdowania elementów, które zaczynają się lub kończą jakimś ciągiem znaków.

//elementy, który klasa zaczyna się ciągiem znaków 'block'
_driver.FindElement(By.CssSelector("[class^='block']"));
//elementy, który klasa kończy się ciągiem znaków 'sellers'
_driver.FindElement(By.CssSelector("[class$='sellers']"));

W selektorach CSS są jeszcze dwie ciekawe rzeczy dotyczące atrybutów. Spójrzmy na poniższy przykład.

<div class="królowa śniegu">
<div class="król złoty">

Powiedzmy, że chcemy napisać test, który zlokalizuje element posiadający klasę „król”. Nie możemy użyć poniższej konstrukcji (zanim przescrollujesz niżej zastanów się dlaczego).

_driver.FindElement(By.CssSelector("[class*='król']"));

Jeżeli Twoją odpowiedzią jest: taki selektor zwróci oba divy, to masz rację. Znak „*” szuka danego ciągu znaków w całym atrybucie, nie interesuje go wcale czy tak naprawdę tam jest kilka klas czy jedna. Szczęśliwie CSS ma odpowiedź także i na to, a mianowicie:

_driver.FindElement(By.CssSelector("[class~='król']"));

Znak ~ działa tak, że jeżeli wartości atrybutu rozdzielone są spacją, to traktuje jak listę i szuka na tej liście dokładnie takiej samej wartości, czyli w naszym wypadku pełnej nazwy klasy.

Jest jeszcze jeden śmieszny myk w selektorach CSS. Nie zdarzyło mi się go użyć, ale nigdy nie wiadomo. Teraz podobny przykład:

<div class="królowa-śniegu">
<div class="król-złoty">

Teraz naszym zadaniem będzie namierzyć, który zaczyna się od „król” i zaraz potem ma myślnik („-„). Możemy to zrobić po staremu, np.:

_driver.FindElement(By.CssSelector("[class^='król-']"));

Ale możemy to też zrobić tak:

_driver.FindElement(By.CssSelector("[class|='król']"));

Ta druga konstrukcja działa w ten sposób, że traktuje myślnik jako separator i szuka takich elementów, których pierwszy element z listy wartości atrybutów jest równy podanemu. Może i trochę dziwne, raczej rzadko używane w zastosowaniach testerskich ale dla porządku podałam, może akurat komuś styknie.

Szukanie elementu po typie

Tutaj prościej być nie może. Chcąc wyszukać wszystkie elementy albo, w kolejnej linijce, pierwszy element typu „input” użyjemy po prostu:

_driver.FindElements(By.CssSelector("input"));
_driver.FindElement(By.CssSelector("input"));

Typ elementu można połączyć z wyszukiwaniem po atrybucie, np. dla przycisku to bestsellerów będzie to:

_driver.FindElement(By.CssSelector("a[class*='bestseller']"));

Będzie to szczególnie przydatne w sytuacji podobnej do tej poniżej:

<div class='btn'>
	<button class='btn'>OK mordeczko!</button>
</div>
<button>OK!</button>

I co tu się dzieje: mamy dwa przyciski, z czego jeden jest w divie, drugi nie. Załóżmy, że chcemy znaleźć ten w divie. Po klasie go nie znajdziemy, bo wtedy div też się załapie, po samym typie elementu też nie, bo to nie jest jedyny element typu button na stronie. Ale jeżeli wykorzystamy obie informacje, element będzie jedynym, którego będzie dotyczył selektor. A idzie to tak:

_driver.FindElement(By.CssSelector("button[class='btn']"));

Co do zawartości w nawiasie kwadratowym: możemy używać wszystkich konstrukcji już nam znanych z poprzedniego punktu o atrybutach, a więc możemy np. szukać elementu typu button, którego klasa zaczyna się od znaków „butto”.

Szukanie elementu za pomocą klasy i ID

Klasę i ID możemy używać jak zwykłych atrybutów ale mamy też do dyspozycji poniższe lokatory:

//elementy o klasie btn
_driver.FindElement(By.CssSelector(".btn"));
//elementy o id button
_driver.FindElement(By.CssSelector("#button"));

Elementy na stronie mają zawsze tylko jedno id więc sprawa jest prosta: id elementu musi być równe podanemu po znaku # żeby element został odnaleziony. Każdy obiekt może mieć natomiast kilka klas. Po kropce podajemy nazwę tylko jednej z nich – wystarczy więc, że tylko jedna klasa będzie zgodna i obiekt będzie pasował do takiego selektora. Tutaj też możemy stosować kombinację z typem elementu, a więc poniższe metody również zadziałają:

//elementy o klasie btn
_driver.FindElement(By.CssSelector("div.btn"));
//elementy o id button
_driver.FindElement(By.CssSelector("div#button"));

Szukanie elementu przez relację

Bywają sytuacje, kiedy nie jesteśmy w stanie odnaleźć elementu w jeden z dotychczas opisanych sposobów albo jesteśmy ale jest to mało efektywne. Dzieje się tak przede wszystkim, gdy szukany przez nas element jest generowany dynamicznie w związku z czym jego atrybuty się zmieniać. Wtedy możemy odwołać się do innych elementów i za ich pomocą zlokalizować szukany przez nas element powołując się na relację między nimi. Możemy tego sposobu użyć, gdy żywcem nie mamy jak namierzyć elementu bezpośrednio, bo zbyt wiele elementów ma te same wartości atrybutów. Użyjemy poniższego fragmentu dokumentu, żeby pokazać jakich zależności i w jaki sposób możemy użyć.

<div class='formularz-rejestracyjny'>
	<div class='formularz-zawodnik'
		<button class='logowanie'>Mam już konto</button>
		<input id='imie'>Podaj imię</input>
		<input id='nazwisko'>Podaj nazwisko</input>
		<button class='rejestracja'>Zarejestruj</button>
	</div>
	<div class='formularz-sedzia'
		<button class='logowanie'>Mam już konto</button>
		<input id='imie'>Podaj imię</input>
		<input id='nazwisko'>Podaj nazwisko</input>
		<button class='rejestracja'>Zarejestruj</button>		
	</div>
</div>

Cztery najprostsze to wskazanie potomka, dziecka oraz rodzeństwa (następującego bezpośrednio i niebezpośrednio po elemencie który wskażemy). Na przykładzie powyższego dokumentu każdą z tych metod możemy wykorzystać w następujący sposób.

Wskazania dziecka dokonujemy używając symbolu „>”, przy czym po lewej stronie znajduje się selektor rodzica a po prawej dziecka, którego szukamy.

//zaznacz input o id=imie który znajduje się w formularzu zawodnika
_driver.FindElement(By.CssSelector(div[class='formularz-zawodnik']>input[id='imie']));
//zaznacz wszystkie inputy o id=imie
_driver.FindElements(By.CssSelector(div>input[id='imie']));

Podobnie wygląda konstrukcja dla potomka ale wtedy zamiast znaku „>” używamy po prostu spacji. Zatem, żeby znaleźć oba buttony służące do logowania, potrzebujemy użyć poniższej metody.

_driver.FindElements(By.CssSelector(div[class='formularz-rejestracyjny'] button[class='logowanie']));

Selektor używający w tym wypadku „>” zamiast spacji zwróci nam pustą listę elementów, bo „div” o klasie formularz-rejestracyjny ma wśród swoich dzieci jedynie dwa divy, żadnego buttona.

Na podobnej zasadzie możemy się powoływać na rodzeństwo. Żeby wskazać pierwszego następującego brata/siostrę elementu używamy znaku „+”.

//zaznacz inputy, które znajdują się bezpośrednio po przycisku logowania - wskaże dwa inputy o id=imie
_driver.FindElements(By.CssSelector(button[class='logowanie']+input));
//zaznacz inputy o id=nazwisko, które znajduje się bezpośrednio po przycisku logowania 
//- wskaże dwa inputy o id=nazwisko
_driver.FindElements(By.CssSelector(button[class='logowanie']+input[id='nazwisko']));

Możemy też zaznaczyć dowolnego następującego brata/siostrę, nie tylko pierwszego następującego. W tym celu używamy znaku „~”.

//zaznacz inputy, które są rodzeństwem buttona logowania i występują po nim - zaznaczy wszystkie cztery inputy
_driver.FindElement(By.CssSelector(button[class='logowanie']~input));

Pozostałe metody

Są też inne sztuczki, które da się zrobić za pomocą selektorów CSS ale mam do nich dużo mniejsze zaufanie. Głównie dlatego, że jeżeli do DOMa zostanie dodany nowe element, to nasze selektory mogą się rozjechać. Mam tutaj głównie na myśli wskazywanie dzieci i rodzeństwa podając jego numer, np. podaj mi trzeci element będący dzieckiem diva o podanej klasie. Wymienię je tutaj i pokrótce wyjaśnię, ale żeby nie było, że nie ostrzegałam.

  • E:nth-child(n) – zwraca element E, który jest n-tym dzieckiem swojego rodzica.
  • E:nth-last-child(n) – zwraca element E, który jest n-tym dzieckiem swojego rodzica licząc wstecz.
  • E:nth-of-type(n) – zwraca element E, który jest n-tym dzieckiem typu E swojego rodzica.
  • E:nth-last-of-type(n) – zwraca element E, który jest n-tym dzieckiem typu E licząc wstecz.
  • E:first-child – zwraca element E, który jest pierwszym dzieckiem swojego rodzica.
  • E:last-child – zwraca element E, który jest ostatnim dzieckiem swojego rodzica.
  • E:first-of-type – zwraca element E, który jest pierwszym bratem elementu tego samego typu.
  • E:last-of-type – zwraca element E, który jest ostatnim bratem elementu tego samego typu.
  • E:only-child – zwraca element E, który jest jedynym dzieckiem swojego rodzica.
  • E:only-of-type – zwraca element E, który jest jedynym rodzeństwem swojego brata.

Ściąga i link do kolejnej części

Z CSSów to tyle – taki zakres jest w zupełności wystarczający do zastosowań testerskich. W codziennej pracy używam może 10% rozwiązań, o których tutaj napisałam. To się niebawem zmieni, bo w trakcie pracy nad tym materiałem przypomniałam sobie o kilku wygodnych konstrukcjach. Wracanie od czasu do czasu do początków jest ważne prawdopodobnie w każdej dziedzinie życia.

Jako ekstra pomoc podrzucam link do ściągi. Jeżeli masz siłę i chęci to tutaj znajdziesz artykuł o XPath – alternatywie dla selektorów CSS. I jak zwykle zachęcam do kontaktu, zawsze chętnie pogadam o testach oraz o tym, co mogę poprawić w moich materiałach, by niosły jeszcze większą wartość dla potomnych 🙂

Dodaj komentarz: