1

I'm really struggling trying to figure out how to stop PHPUnit+Selenium from breaking once my tests try to move from one page to another. For example, I do something like this:

public function myTest()
{
    $this->clickOnElement('somelink');
    $this->assertEquals('content', $this->byId('newElement'));
}

When I try to run this, I get an error like this:

1) myTestClass::myTest
PHPUnit_Extensions_Selenium2TestCase_WebDriverException: Element not found in the cache
   - perhaps the page has changed since it was looked up
Command duration or timeout: 107 milliseconds

The problem only appears when moving between two pages, and the element you are looking for exists on both pages (like a heading or title.) There are actually two problems here, both of which are race conditions:

  1. clickOnElement() starts the next page loading, but before that happens byId() runs on the current page and finds the element. Then the browser finishes loading the new page, and assertContains() tries to get the value out of the element. But the new page has now loaded, and the element reference we have is from the previous page. Because that element no longer exists in the current DOM, you get the error about the stale element.

  2. clickOnElement() starts the next page loading, but before it has finished loading byId() runs on the new, but not fully loaded page. Because the page hasn't finished loading, you get an 'element not found' error.

Any ideas how to fix these two problems?

Malvineous
  • 25,144
  • 16
  • 116
  • 151
  • Can I ask an obvious question: are you sure 'somelink' is actually on the page? Your code sample looks like it *should work* in a normal scenario. – scipilot May 21 '14 at 12:52
  • Also try breaking down to what clickOnElement shortcuts: `$this->element($this->using('id')->value($id))->click();` but do it in stages and test the element exists first. – scipilot May 21 '14 at 12:53
  • It does exist on the page because some tests will fail the first few times but then succeed later, i.e. there's a race condition somewhere. But then tests like this fail all the time. Also if the element doesn't exist on the page I get an 'element not found' error instead of the one above, so I'm confident that's not the cause of the problem. – Malvineous May 21 '14 at 21:23
  • Is the element in the original page source provided by the backend by the time the body onload fires? Or is it being dynamically generated by JavaScript later? Or any other ansyc process such as Api callback? – scipilot May 22 '14 at 22:27
  • Re lack of documentation, See [this question and my article referenced](http://stackoverflow.com/questions/19106365/where-the-heck-is-any-selenium-2-documentation-for-phpunit) – scipilot May 22 '14 at 22:33
  • @scipilot: Thanks for the pointers. They helped a lot, and I have now narrowed down the exact problem - see the update to my question. – Malvineous May 23 '14 at 00:24

1 Answers1

2

Ok, I think I figured this one out. The second problem (looking for elements before the page has loaded) can be fixed by setting an implicitWait() value:

public function setUpPage()
{
    // If an element cannot be found because the page is still loading, keep
    // trying for 5000ms before failing
    $this->timeouts()->implicitWait(5000);
}

This only needs to be called once for each test, so putting it in setUpPage() is a good place. It will cause Selenium to keep looking for up to five seconds before complaining that it can't find a given element. Five seconds should be plenty of time for the page to finish loading and the element to become accessible.

The first problem (waiting until the browser has navigated away from the current page) requires some changes to the test code to work. First we create a new function called waitForURLChange() and then in the example below we call it whenever we expect the browser to have navigated to a new URL.

private $urlPrevious;
/// Wait for the browser URL to change.  Must be called once before the test starts
/// to read in the initial URL.
public function waitForURLChange()
{
    if (!empty($this->urlPrevious)) {
        $prev = $this->urlPrevious; // workaround for PHP limitation with $this and closures
        $this->waitUntil(
            function($testCase) use ($prev) {
                return strcmp($testCase->url(), $prev) ? 1 : null;
            },
            2000 // milliseconds to wait before giving up if the URL hasn't changed
        );
    }

    // Make sure the URL has actually changed (in case of timeout above)
    $this->assertNotEquals($this->urlPrevious, $this->url());

    $this->urlPrevious = $this->url();
}

Now in your test code, you just call waitForURLChange() after each action that causes a page transition:

public function myTest()
{
    // Call the function once first to read in the current URL.  This does not wait.  You
    // could put this in setUpPage() instead so you don't need it at the start of every test.
    $this->waitForURLChange();

    // Do something that causes the browser to move to a different page.
    $this->clickOnElement('somelink');

    // Call the function again, and it won't return until the browser has started
    // loading the new page.
    $this->waitForURLChange();

    // Now perform the test, knowing the page may not have fully loaded yet but you are
    // definitely not stuck on the previous page.
    $this->assertEquals('content', $this->byId('newElement'));

    // Click the back button in the browser (for example).
    $this->back();

    // Wait until we're back at the previous page.
    $this->waitForURLChange();

    // ...and so on...
}
Malvineous
  • 25,144
  • 16
  • 116
  • 151