<?php
namespace Laravel\Dusk;
use Exception;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
class ElementResolver
{
use Macroable;
/**
* The remote web driver instance.
*
* @var \Facebook\WebDriver\Remote\RemoteWebDriver
*/
public $driver;
/**
* The selector prefix for the resolver.
*
* @var string
*/
public $prefix;
/**
* Set the elements the resolver should use as shortcuts.
*
* @var array
*/
public $elements = [];
/**
* The button finding methods.
*
* @var array
*/
protected $buttonFinders = [
'findById',
'findButtonBySelector',
'findButtonByName',
'findButtonByValue',
'findButtonByText',
];
/**
* Create a new element resolver instance.
*
* @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver
* @param string $prefix
* @return void
*/
public function __construct($driver, $prefix = 'body')
{
$this->driver = $driver;
$this->prefix = trim($prefix);
}
/**
* Set the page elements the resolver should use as shortcuts.
*
* @param array $elements
* @return $this
*/
public function pageElements(array $elements)
{
$this->elements = $elements;
return $this;
}
/**
* Resolve the element for a given input "field".
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForTyping($field)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
return $this->firstOrFail([
"input[name='{$field}']", "textarea[name='{$field}']", $field,
]);
}
/**
* Resolve the element for a given select "field".
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForSelection($field)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
return $this->firstOrFail([
"select[name='{$field}']", $field,
]);
}
/**
* Resolve all the options with the given value on the select field.
*
* @param string $field
* @param array $values
* @return \Facebook\WebDriver\Remote\RemoteWebElement[]
*
* @throws \Exception
*/
public function resolveSelectOptions($field, array $values)
{
$options = $this->resolveForSelection($field)
->findElements(WebDriverBy::tagName('option'));
if (empty($options)) {
return [];
}
return array_filter($options, function ($option) use ($values) {
return in_array($option->getAttribute('value'), $values);
});
}
/**
* Resolve the element for a given radio "field" / value.
*
* @param string $field
* @param string|null $value
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
* @throws \InvalidArgumentException
*/
public function resolveForRadioSelection($field, $value = null)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
if (is_null($value)) {
throw new InvalidArgumentException(
"No value was provided for radio button [{$field}]."
);
}
return $this->firstOrFail([
"input[type=radio][name='{$field}'][value='{$value}']", $field,
]);
}
/**
* Resolve the element for a given checkbox "field".
*
* @param string|null $field
* @param string|null $value
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForChecking($field, $value = null)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
$selector = 'input[type=checkbox]';
if (! is_null($field)) {
$selector .= "[name='{$field}']";
}
if (! is_null($value)) {
$selector .= "[value='{$value}']";
}
return $this->firstOrFail([
$selector, $field,
]);
}
/**
* Resolve the element for a given file "field".
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForAttachment($field)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
return $this->firstOrFail([
"input[type=file][name='{$field}']", $field,
]);
}
/**
* Resolve the element for a given "field".
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForField($field)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
return $this->firstOrFail([
"input[name='{$field}']", "textarea[name='{$field}']",
"select[name='{$field}']", "button[name='{$field}']", $field,
]);
}
/**
* Resolve the element for a given button.
*
* @param string $button
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \InvalidArgumentException
*/
public function resolveForButtonPress($button)
{
foreach ($this->buttonFinders as $method) {
if (! is_null($element = $this->{$method}($button))) {
return $element;
}
}
throw new InvalidArgumentException(
"Unable to locate button [{$button}]."
);
}
/**
* Resolve the element for a given button by selector.
*
* @param string $button
* @return \Facebook\WebDriver\Remote\RemoteWebElement|null
*/
protected function findButtonBySelector($button)
{
if (! is_null($element = $this->find($button))) {
return $element;
}
}
/**
* Resolve the element for a given button by name.
*
* @param string $button
* @return \Facebook\WebDriver\Remote\RemoteWebElement|null
*/
protected function findButtonByName($button)
{
if (! is_null($element = $this->find("input[type=submit][name='{$button}']")) ||
! is_null($element = $this->find("input[type=button][value='{$button}']")) ||
! is_null($element = $this->find("button[name='{$button}']"))) {
return $element;
}
}
/**
* Resolve the element for a given button by value.
*
* @param string $button
* @return \Facebook\WebDriver\Remote\RemoteWebElement|null
*/
protected function findButtonByValue($button)
{
foreach ($this->all('input[type=submit]') as $element) {
if ($element->getAttribute('value') === $button) {
return $element;
}
}
}
/**
* Resolve the element for a given button by text.
*
* @param string $button
* @return \Facebook\WebDriver\Remote\RemoteWebElement|null
*/
protected function findButtonByText($button)
{
foreach ($this->all('button') as $element) {
if (Str::contains($element->getText(), $button)) {
return $element;
}
}
}
/**
* Attempt to find the selector by ID.
*
* @param string $selector
* @return \Facebook\WebDriver\Remote\RemoteWebElement|null
*/
protected function findById($selector)
{
if (preg_match('/^#[\w\-:]+$/', $selector)) {
return $this->driver->findElement(WebDriverBy::id(substr($selector, 1)));
}
}
/**
* Find an element by the given selector or return null.
*
* @param string $selector
* @return \Facebook\WebDriver\Remote\RemoteWebElement|null
*/
public function find($selector)
{
try {
return $this->findOrFail($selector);
} catch (Exception $e) {
//
}
}
/**
* Get the first element matching the given selectors.
*
* @param array $selectors
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function firstOrFail($selectors)
{
foreach ((array) $selectors as $selector) {
try {
return $this->findOrFail($selector);
} catch (Exception $e) {
//
}
}
throw $e;
}
/**
* Find an element by the given selector or throw an exception.
*
* @param string $selector
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*/
public function findOrFail($selector)
{
if (! is_null($element = $this->findById($selector))) {
return $element;
}
return $this->driver->findElement(
WebDriverBy::cssSelector($this->format($selector))
);
}
/**
* Find the elements by the given selector or return an empty array.
*
* @param string $selector
* @return \Facebook\WebDriver\Remote\RemoteWebElement[]
*/
public function all($selector)
{
try {
return $this->driver->findElements(
WebDriverBy::cssSelector($this->format($selector))
);
} catch (Exception $e) {
//
}
return [];
}
/**
* Format the given selector with the current prefix.
*
* @param string $selector
* @return string
*/
public function format($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 (Str::startsWith($selector, '@') && $selector === $originalSelector) {
$selector = '[dusk="'.explode('@', $selector)[1].'"]';
}
return trim($this->prefix.' '.$selector);
}
}