<?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); } }