8

I have an element with a class a. So, in Selenium code I am getting with this:

WebElement element = driver.findElement(By.cssSelector(".a"));

Afterwards I am clicking on it with element.click();. The click event removes the class a from the element - which exactly is the test case I am trying to execute.

So, now I wanted to ask the element if it already owns this class:

element.getAttribute("class").contains("a");

But this did not work because the WebElement tried to find the element again by the given selector which was not clear to me. I thought the WebElement, once found, is internally copied throughout the scope. But obviously, it calls the linked selector everytime it is called in the code.

So, how can I retrieve an element more persistantly? How can I avoid the WebElement being refreshed on every call to track the changes of the already selected element?

Of course, I could use a work-around using the DOM, the parents or a list id. But I really want to avoid this, because I do not want to get too much information about the DOM structure into my test code. This is the reason why I added classes and ids.


Edit: Adding the log output:

WebElement element = driver.findElement(By.cssSelector(".a"));

1564042692783   webdriver::server   DEBUG   -> POST /session/2d7cce7d-bd10-4814-b619-b4c8dc212fac/elements {"value":".a","using":"css selector"}
1564042692787   Marionette  TRACE   0 -> [0,10,"WebDriver:FindElements",{"using":"css selector","value":".a"}]
1564042692793   Marionette  TRACE   0 <- [1,10,null,[{"element-6066-11e4-a52e-4f735466cecf":"517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a","ELEMENT":"517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a"}]]
1564042692794   webdriver::server   DEBUG   <- 200 OK {"value":[{"element-6066-11e4-a52e-4f735466cecf":"517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a"}]}

As you can see, the received element is 517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a.

element.click();

1564042703055   webdriver::server   DEBUG   -> POST /session/2d7cce7d-bd10-4814-b619-b4c8dc212fac/elements {"value":".a","using":"css selector"}
1564042703058   Marionette  TRACE   0 -> [0,11,"WebDriver:FindElements",{"using":"css selector","value":".a"}]
1564042703065   Marionette  TRACE   0 <- [1,11,null,[{"element-6066-11e4-a52e-4f735466cecf":"517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a","ELEMENT":"517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a"}]]
1564042703066   webdriver::server   DEBUG   <- 200 OK {"value":[{"element-6066-11e4-a52e-4f735466cecf":"517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a"}]}
1564042703142   webdriver::server   DEBUG   -> POST /session/2d7cce7d-bd10-4814-b619-b4c8dc212fac/element/517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a/click {"id":"517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a"}
1564042703145   Marionette  TRACE   0 -> [0,12,"WebDriver:ElementClick",{"id":"517f4e9c-5d09-4fe0-8c34-4d8c153a9c4a"}]
1564042703380   Marionette  DEBUG   Canceled page load listener because no navigation has been detected
1564042703382   Marionette  TRACE   0 <- [1,12,null,{}]
1564042703384   webdriver::server   DEBUG   <- 200 OK {"value":null}

And now the check:

element.getAttribute("class");

1564042714064   webdriver::server   DEBUG   -> POST /session/2d7cce7d-bd10-4814-b619-b4c8dc212fac/elements {"value":".a","using":"css selector"}
1564042714067   Marionette  TRACE   0 -> [0,13,"WebDriver:FindElements",{"using":"css selector","value":".a"}]
1564042714070   Marionette  TRACE   0 <- [1,13,null,[]]
1564042714071   webdriver::server   DEBUG   <- 200 OK {"value":[]}

As you can see, no element has been returned now.


Edit: After evaluating the solution of @RahulL (which seems to work; at the click execution no further WebDriver:FindElements call is logged - in contrast to my log) I believe that the problem lies somewhere in the Aquillian Graphene implementation which wraps my Selenium. The findElement() call does not call the Selenium class directly. That's why I added these tags. It could be relevant for finding the problem.

So, the class definition:

import static org.junit.Assert.assertFalse;

import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.junit.Arquillian;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

@RunWith(Arquillian.class)
@RunAsClient
public class MyTests {

    @Drone
    WebDriver driver;

    @Test
    public void test_removeClassFromElement() {
        driver.navigate().refresh();
        driver.get("my.application");

        WebElement element = driver.findElement(By.className("a"))
        element.click();

        assertFalse(
            element .getAttribute("class").contains("a")
        );
    }
}

and the arquillian.xml:

<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns="http://jboss.org/schema/arquillian"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.org/schema/arquillian
        http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

    <extension qualifier="webdriver">
        <property name="browser">firefox</property>
        <property name="firefoxLogLevel">FINEST</property>
    </extension>

</arquillian>
S-Man
  • 22,521
  • 7
  • 40
  • 63
  • 2
    AFAIK selenium will refresh the elements with the `click` event (atleast in your case). So, you have to check if the size of `findElements` with class is returned `0` after click. – supputuri Jul 22 '19 at 13:52
  • @supputuri yes, but can I avoid this refresh? How could I compare the states of the same element before and after the click if the only possible selector has been changed through this click? – S-Man Jul 22 '19 at 13:54
  • 2
    AFAIK, the answer is NO. You might have to relay on the other elements like parent or siblings to determine the element. – supputuri Jul 22 '19 at 14:00
  • @S-Man when you click to the element does page or element refresh? – Sers Jul 24 '19 at 19:34
  • if element is clicked/refreshed and its state is changed in DOM and we refer it again without findElement then it throws StaleElementException. – Amit Jain Jul 24 '19 at 20:00
  • In my example at the click only the class is removed which causes some styling effects. The objects itself remains at the same position in the DOM, nothing else changed. I just want to test if the click really removes the class. – S-Man Jul 24 '19 at 21:45
  • While this isn't a solution for your chosen approach... In this case I would use a different selector for the element that doesn't change, which is a general approach I take regardless if I'm checking for attribute changes or not. – mrfreester Jul 29 '19 at 19:21
  • @mrfreester Yes, of course. Actually I am using a work-around by selecting the parent. But nevertheless I am curious about this single fact. Just wanted to learn about the technology. – S-Man Jul 29 '19 at 19:46

1 Answers1

4

Tested your scenario in JAVA binding + Firefox driver . Click remove the 'mystyle' from div and able to get the attribute class without 'mystyle'

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.mystyle {
  width: 100%;
  padding: 25px;
  background-color: coral;
  color: white;
  font-size: 25px;
  box-sizing: border-box;
}
</style>
</head>
<body>

<p>Click the "Try it" button to toggle between adding and removing the "mystyle" class name of the DIV element:</p>



<div id="myDIV" onclick="myFunction()" class="mystyle">
This is a DIV element.

</div>

<script>
function myFunction() {
   var element = document.getElementById("myDIV");
   element.classList.remove("mystyle");
}
</script>

</body>
</html>

JAVA Code :

         WebElement element = driver.findElement(By.className("mystyle"));
         element.click();
         System.out.println(element.getAttribute("class"));

Enabled the FirefoxDriverLogLevel.TRACE . WebDriver logs

  1. Find Element

    WebElement element = driver.findElement(By.className("mystyle"));

    webdriver::server   DEBUG   -> POST /session/81662ee0-1195-4ff3-8687-667f3607ea89/element {
      "value": ".mystyle",
      "using": "css selector"
    }
    
  2. Got the element

    webdriver::server DEBUG <- 200 OK {"value":{"element-6066-11e4-a52e-4f735466cecf":"c15725f8-89f9-4fec-af08-be1b9487defe"}}

  3. Now click on the element

    element.click();

    webdriver::server   DEBUG   -> POST /session/81662ee0-1195-4ff3-8687-667f3607ea89/element/c15725f8-89f9-4fec-af08-be1b9487defe/click {
      "id": "c15725f8-89f9-4fec-af08-be1b9487defe" }
    
  4. Now send 'getAttribute'

    element.getAttribute("class")

    webdriver::server DEBUG -> POST /session/81662ee0-1195-4ff3-8687-667f3607ea89/execute/sync { "script": "here selenium sends the getAttribute script", "args": [ { "element-6066-11e4-a52e-4f735466cecf": "c15725f8-89f9-4fec-af08-be1b9487defe" }, "class" ] }

5 Response :

webdriver::server   DEBUG   <- 200 OK {"value":""}

Got the value of class as "" . ".mystyle" is not present

From above logs once element is found using selenium and assigned to 'element' It does not send post request to find element again. c15725f8-89f9-4fec-af08-be1b9487defe remains same in click as well as getAttribute. (Webdriver Protocol)

If you are using the @FindBy annotations in JAVA binding then Selenium will try to find the element again and again on every action using proxy .This will avoid the staleelemnt exception in most of the cases.

To avoid flakiness and retrieve an element more persistently you will need to use attributes that do not change.

Rahul L
  • 4,249
  • 15
  • 18
  • Thanks for your work! But nevertheless, my behaviour remains the same as described in the question. I added my log to the question. – S-Man Jul 25 '19 at 08:21
  • @S-Man The logs that you have added for element.getAttribute("class"); . -> POST /session/2d7cce7d-bd10-4814-b619-b4c8dc212fac/elements I think it should be something like -> POST /session/81662ee0-1195-4ff3-8687-667f3607ea89/execute/sync . Is there any findelement call in between ? – Rahul L Jul 25 '19 at 08:38
  • No, it's just the session id. As you can see the line after: There is call for findElements() again. And this gives out an empty array. Can you show your whole log? – S-Man Jul 25 '19 at 08:45
  • Can not post in the comment too long . Are you using @FindBy ? This post action will happen in every call if you are using that. – Rahul L Jul 25 '19 at 08:49
  • DEBUG 0 -> [0,5,"WebDriver:ElementClick",{"id":"c15725f8-89f9-4fec-af08-be1b9487defe"}] Marionette DEBUG [8589934593] Canceled page load listener because no navigation has been detected Marionette DEBUG 0 <- [1,5,null,{"value":null}] webdriver::server DEBUG <- 200 OK {"value":null} webdriver::server DEBUG -> POST /session/81662ee0-1195-4ff3-8687-667f3607ea89/execute/sync { "script": "return (function(){}", "args": [ { "element-6066-11e4-a52e-4f735466cecf": "c15725f8-89f9-4fec-af08-be1b9487defe" }, "class" ] } – Rahul L Jul 25 '19 at 08:51
  • No, in this case I am not using @FindBy. Are you sure that your last answer with "value":"" really is because the element has no class? Maybe it returns the value also when there is no element found. Could you please check it if your element has two classes. Then you would return at least one in the end – S-Man Jul 25 '19 at 08:59
  • webdriver::server DEBUG -> POST /session/bcd095af-6952-43dc-96a7-b801e761c275/execute/sync { "script": "script ", "args": [ { "element-6066-11e4-a52e-4f735466cecf": "34cf097c-a49a-4353-ba93-378da22747c3" }, "class" ] } Marionette DEBUG 0 -> [0,6,"WebDriver:ExecuteScript",{"args":[{"script here"}] Marionette DEBUG 0 <- [1,6,null,{"value":"secondclass"}] webdriver::server DEBUG <- 200 OK {"value":"secondclass"} Got the second class value. – Rahul L Jul 25 '19 at 09:21