Dusk: Other selectors than css? xpath?

Created on 27 Jan 2017  路  7Comments  路  Source: laravel/dusk

Might it be possible to support different selectors than the cssSelector?
The webdriver also supports xpath selectors.
It would be great if functions like text() will support xpath also.

I've played a little bit with the Dusk code and added a parameter to the text() function and it seems to be very easy. But i'm not sure if this is the best way.
Dusk might also detect xpath by itself (by search for // at the beginning, for example).

What do you think?

enhancement

Most helpful comment

I suspect a lot of people will come across this issue when trying to click buttons that have no id/name/text and a child element (e.g. span) with the displayed text.

All 7 comments

Ok a regex will not work, as xpath not only starts with //.
So maybye parameters to the functions like text() or passing a WebDriverBy object as selector?
But this might also become difficult for functions like clickLink which uses the selector as a string.
I really want to help with that, but i don't know what the best idea may be.

@taylorotwell does such a feature makes sense?

Hi,

i had the same problem.

My current workaround is to call the driver directly via:

$browser->driver->findElement( WebDriverBy::xpath("//table[@class='x-grid3-row-table']/tbody/tr/td/div/a[contains(text(),'$value')]") )
                ->click();

Or here a more "dynamic" way with the latest version (tested with the master branch).
(For details, please check: https://github.com/laravel/dusk/pull/119)

You can now extend the existing browser class, this allows us the following:

app\Testing\MyBrowser.php:

<?php

namespace App\Testing;

use Laravel\Dusk\Browser;

class MyBrowser extends Browser {

    /**
     * Create a browser instance.
     *
     * @param  \Facebook\WebDriver\Remote\RemoteWebDriver  $driver
     * @param  ElementResolver  $resolver
     * @return void
     */
    public function __construct($driver, $resolver = null)
    {
        $this->driver = $driver;

        $this->resolver = $resolver ?: new DynamicElementResolver($driver);
    }

    /**
     * Find an element by the given selector or return null.
     *
     * @param  string|\Facebook\WebDriver\WebDriverBy  $selector
     * @return \Facebook\WebDriver\Remote\RemoteWebElement|null
     */
    public function findBySelector($selector) {
        return $this->resolver->find($selector);
    }

}

app\Testing\DynamicElementResolver.php

<?php

namespace App\Testing;

use Laravel\Dusk\ElementResolver;
use Facebook\WebDriver\WebDriverBy;

class DynamicElementResolver extends ElementResolver
{  
    /**
     * Find an element by the given selector or throw an exception.
     *
     * @param  string  $selector
     * @return \Facebook\WebDriver\Remote\RemoteWebElement
     */
    public function findOrFail($selector)
    {
        return $this->driver->findElement(
            (is_string($selector)) ? WebDriverBy::cssSelector($this->format($selector)) : $selector
        );
    }

    /**
     * Find the elements by the given selector or return an empty array.
     *
     * @param  string  $selector
     * @return array
     */
    public function all($selector)
    {
        try {
            return $this->driver->findElements(
                (is_string($selector)) ? WebDriverBy::cssSelector($this->format($selector)) : $selector
            );
        } catch (Exception $e) {
            //
        }

        return [];
    }
}

tests\DuskTestCase.php

<?php

namespace Tests;

//...
use App\Testing\MyBrowser;


abstract class DuskTestCase extends BaseTestCase
{
    //...

    /**
     * Create a new Browser instance.
     *
     * @param  \Facebook\WebDriver\Remote\RemoteWebDriver  $driver
     * @return \Laravel\Dusk\Browser
     */
    protected function newBrowser($driver)
    {
        return new MyBrowser($driver);
    }
}

Last but not least, here a simple test:

test\Browser\ExampleTest.php

<?php

namespace Tests\Browser;


use Tests\DuskTestCase;
use App\Testing\MyBrowser;
use Facebook\WebDriver\WebDriverBy;

class ExampleTest extends DuskTestCase
{

    public function testWithCss() {
        $this->browse(function (MyBrowser $browser) {
            $browser->resize(1200, 900);
            $browser->visit('https://www.laravel.com');
            $browser->findBySelector('.nav-docs'));
        });
    }

    public function testWithXPath() {
        $this->browse(function (MyBrowser $browser) {
            $browser->resize(1200, 900);
            $browser->visit('https://www.laravel.com');
            $browser->findBySelector(WebDriverBy::xpath("//li[@class='nav-docs']"));
        });
    }
}

I kind of wish the following was in InteractsWithElements. I get people not wanting to have to know the specifics of Xpath, but there are some things that you need it for.

  /**
   * Resolve the element for a given tag by text.
   */
  protected function findTagContaingText($tag, $text)
  {
    return $this->driver->findElement(
        WebDriverBy::xpath("//".$tag."[contains(text(), '".$text."')]") 
    );
  }

I've created my own browser class and added it. I'd make a PR if there was any interest.

I suspect a lot of people will come across this issue when trying to click buttons that have no id/name/text and a child element (e.g. span) with the displayed text.

Adding the following method to @noxify's app\Testing\MyBrowser.php above allows you to call existing Browser methods (e.g. attribute(), assert*(), etc.) with WebDriverBys:

  /**
   * Format the given selector with the current prefix if selector is a string.
   *
   * @param  string|WebDriverBy $selector
   * @return string|WebDriverBy
   */
  public function format($selector)
  {
    if (is_string($selector)) {
      $sortedElements = collect($this->elements)->sortByDesc(function ($element, $key) {
        return strlen($key);
      })->toArray();

      $selector = str_replace(
        array_keys($sortedElements), array_values($sortedElements), $originalSelector = $selector
      );

      if (starts_with($selector, '@') && $selector === $originalSelector) {
        $selector = '[dusk="'.explode('@', $selector)[1].'"]';
      }

      return trim($this->prefix.' '.$selector);
    }
    return $selector->getValue();
  }

I've also done the following to help clean up my tests.

  • Added newBrowser() to a new DuskTestCaseWithXPath class that extends the original DuskTestCase (instead of adding newBrowser() to the original DuskTestCase since not all of my test classes require XPath functionality)
  • Added xpaths() (to mirror elements()) that returns an array of '@alias' => new XPath(..) to custom BasePage class, which extends Laravel\Dusk\Page. The XPath class is defined as:
/**
<?php

namespace Tests\Browser\Helpers;

use Facebook\WebDriver\WebDriverBy;

/**
 * Represent an XPath and provide utilities to modify the XPath.
 */
class XPath extends WebDriverBy {

  /**
   * Construct a new XPath instance.
   * @param String $xpath  XPath
   */
  public function __construct(String $xpath) {
    parent::__construct('xpath', $xpath);
  }

  /**
   * Modify selector to require that this XPath have the specified value.
   * @param  String $value  Value that this XPath must have
   * @return XPath  A new instance of XPath with modified selector
   */
  public function withValue(String $value) {
    return new self($this->getValue() . "[@value='$value']");
  }

}
  • Added xpath() to custom BasePage class that gets an XPath by its alias
  /**
   * Get XPath by alias.
   * @return  String  XPath
   */
  public function xpath(String $alias) {
    return $this->xpaths()[$alias];
  }

Doing these things allows you to write tests that look like

  ...
  /** @test */
  public function page_ShowsFilter() {
    $this->browse(function (MyBrowser $browser) {
      $page = new Page();
      $browser->loginAs($this->user)
              ->visit($page)
              ->waitForText('Some text')
              ->assertVisible($page->xpath('@filter-heading'))
              ->assertVisible($page->xpath('@filter-option')->withValue('Some filter'));
    });
  }

This is definitely a workaround, but I hope this might be helpful for someone in the meantime.

personally... xpath should be embraced. Yesyes... "the only thing we know is css" but with xpath you can do things like selecting the parent or the jquery .closest functions. CSS is a little bit the stupid cousin of xpath.

Is it possible to select an element that contains text 'click here' with css? no... parent? no.

and learning xpath is basically "open this website here https://devhints.io/xpath#selectors "

examples

//product[@price > 2.50]

$('li').closest('section') ------>  //li/ancestor-or-self::section

Text match ------> //button[text()="Submit"]

//button[contains(text(),"Go")]

$('ul > li').parent() ------>  //ul/li/..

//ul[count(li) > 2]

Was this page helpful?
0 / 5 - 0 ratings