3

I'm using Behat 3 and PhantomJS 2. Currently I have a scenario defined as such:

@javascript
Scenario: I visit the blog through the Blog & Events menu.
  Given I am an anonymous user
  And I am on the homepage
  And I follow "Link Text"
  Then I should be on "/path-to-page"

When I run this with Goutte it's fine. When I run this with vanilla Selenium, it's fine (it launches a browser I can see). However, when I configure Selenium to point the webdriver host to PhantomJS, it explodes on Then I should be on "/path-to-page" claiming it's still on /.

If I add the following wait step:

@javascript
Scenario: I visit the blog through the Blog & Events menu.
  Given I am an anonymous user
  And I am on the homepage
  And I follow "Link Text"
  And I wait 4 seconds
  Then I should be on "/path-to-page"

Then my scenario passes in the green, all good.

Is there a way to get PhantomJS to wait for the page to load before checking the current path? I don't want to depend on arbitrary timeouts. I need a headless solution and PhantomJS seems to be pretty well supported, but if I can't do something as simple as clicking a link and verifying the page that was loaded without adding random waiting steps everywhere, I might need to re-evaluate my decision.

Lester Peabody
  • 1,868
  • 3
  • 20
  • 42

3 Answers3

1

I was having the same issue, and doing something like this fails because its using the state of the current url:

 $this->getSession()->wait(10000, "document.readyState === 'complete'");

So my workaround for this was adding a variable to the page every time a step is done. When I link is clicked, the variable will no longer exist, this will guarantee that i'm working with a different page.

/**
 * @AfterStep
 */
public function setStepStatus()
{
    $this->getSession()->evaluateScript('window.behatStepHasCompleted = true;');
}

/**
 * @When /^(?:|I )wait for the page to be loaded$/
 */
public function waitForThePageToBeLoaded()
{
    $this->getSession()->wait(10000, "!window.behatStepHasCompleted && document.readyState === 'complete'");
}
Ghostff
  • 1,407
  • 3
  • 18
  • 30
0

Try using this implicit wait in your feature context. In my experience it has helped.

/**
 * @BeforeStep
 */
public function implicitlyWait($event)
{
    // Set up implicit timeouts
    $driver = $this->getSession()->getDriver()->getWebDriverSession();
    $driver->timeouts()->implicit_wait(array("ms" => 10000));
}
KyleFairns
  • 2,947
  • 1
  • 15
  • 35
0

You can always make use of a closure function to encapsule your steps, just as mentioned in the docs. Through it, you can get your steps to run when they're ready. Let's implement a spinner function:

public function spinner($closure, $secs) {
    for ($i = 0; $i <= $secs; $i++) {
        try {
            $closure();
            return;
        }
        catch (Exception $e) {
            if ($i == $secs) {
                throw $e;
            }
        }
        sleep(1);
    }
}

What we're doing here is wait for a number of seconds for the closure function to run successfully. When the time's run out, throw an exception, for we want to know when something's not behaving correctly.

Now let's wrap your function to assert you're in the right page within the spinner:

public function iShouldBeOnPage($wantedUrl) {
    $this->spinner(function() use($wantedUrl) {
        $currentUrl = $this->getSession()->getCurrentUrl();
        if ($currentUrl == $wantedUrl) {
            return;
        }
        else {
            throw new Exception("url is $currentUrl, not $wantedUrl");
        }
    }, 30);

What we're doing here is wait up to 30 seconds to be on the url we want to be after clicking the button. It will not wait for 30 secs, but for as many secs we need until current url is the url we need to be at. Applying it in your function within the *Context.php will result in it being applied in every step you call it within your Gherkin files.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Víctor
  • 533
  • 3
  • 7