updated-packages

This commit is contained in:
RafficMohammed
2023-01-08 00:13:22 +05:30
parent 3ff7df7487
commit da241bacb6
12659 changed files with 563377 additions and 510538 deletions

View File

@@ -2,12 +2,14 @@
namespace Laravel\Dusk;
use Closure;
use BadMethodCallException;
use Closure;
use Facebook\WebDriver\Remote\WebDriverBrowserType;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\WebDriverPoint;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\Remote\WebDriverBrowserType;
class Browser
{
@@ -17,6 +19,7 @@ class Browser
Concerns\InteractsWithJavascript,
Concerns\InteractsWithMouse,
Concerns\MakesAssertions,
Concerns\MakesUrlAssertions,
Concerns\WaitsForElements,
Macroable {
__call as macroCall;
@@ -43,6 +46,13 @@ class Browser
*/
public static $storeConsoleLogAt;
/**
* The directory where source code snapshots will be stored.
*
* @var string
*/
public static $storeSourceAt;
/**
* The browsers that support retrieving logs.
*
@@ -77,7 +87,7 @@ class Browser
/**
* The element resolver instance.
*
* @var ElementResolver
* @var \Laravel\Dusk\ElementResolver
*/
public $resolver;
@@ -95,11 +105,18 @@ class Browser
*/
public $component;
/**
* Indicates that the browser should be resized to fit the entire "body" before screenshotting failures.
*
* @var bool
*/
public $fitOnFailure = true;
/**
* Create a browser instance.
*
* @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver
* @param ElementResolver $resolver
* @param \Laravel\Dusk\ElementResolver $resolver
* @return void
*/
public function __construct($driver, $resolver = null)
@@ -164,6 +181,21 @@ class Browser
* @return $this
*/
public function on($page)
{
$this->onWithoutAssert($page);
$page->assert($this);
return $this;
}
/**
* Set the current page object without executing the assertions.
*
* @param mixed $page
* @return $this
*/
public function onWithoutAssert($page)
{
$this->page = $page;
@@ -174,8 +206,6 @@ class Browser
$page::siteElements(), $page->elements()
));
$page->assert($this);
return $this;
}
@@ -231,6 +261,81 @@ class Browser
return $this;
}
/**
* Make the browser window as large as the content.
*
* @return $this
*/
public function fitContent()
{
$this->driver->switchTo()->defaultContent();
$html = $this->driver->findElement(WebDriverBy::tagName('html'));
if (! empty($html) && ($html->getSize()->getWidth() <= 0 || $html->getSize()->getHeight() <= 0)) {
$this->resize($html->getSize()->getWidth(), $html->getSize()->getHeight());
}
return $this;
}
/**
* Disable fit on failures.
*
* @return $this
*/
public function disableFitOnFailure()
{
$this->fitOnFailure = false;
return $this;
}
/**
* Enable fit on failures.
*
* @return $this
*/
public function enableFitOnFailure()
{
$this->fitOnFailure = true;
return $this;
}
/**
* Move the browser window.
*
* @param int $x
* @param int $y
* @return $this
*/
public function move($x, $y)
{
$this->driver->manage()->window()->setPosition(
new WebDriverPoint($x, $y)
);
return $this;
}
/**
* Scroll screen to element at the given selector.
*
* @param string $selector
* @return $this
*/
public function scrollTo($selector)
{
$this->ensurejQueryIsAvailable();
$selector = addslashes($this->resolver->format($selector));
$this->driver->executeScript("jQuery(\"html, body\").animate({scrollTop: jQuery(\"$selector\").offset().top}, 0);");
return $this;
}
/**
* Take a screenshot and store it with the given name.
*
@@ -239,9 +344,15 @@ class Browser
*/
public function screenshot($name)
{
$this->driver->takeScreenshot(
sprintf('%s/%s.png', rtrim(static::$storeScreenshotsAt, '/'), $name)
);
$filePath = sprintf('%s/%s.png', rtrim(static::$storeScreenshotsAt, '/'), $name);
$directoryPath = dirname($filePath);
if (! is_dir($directoryPath)) {
mkdir($directoryPath, 0777, true);
}
$this->driver->takeScreenshot($filePath);
return $this;
}
@@ -257,10 +368,9 @@ class Browser
if (in_array($this->driver->getCapabilities()->getBrowserName(), static::$supportsRemoteLogs)) {
$console = $this->driver->manage()->getLog('browser');
if (!empty($console)) {
if (! empty($console)) {
file_put_contents(
sprintf('%s/%s.log', rtrim(static::$storeConsoleLogAt, '/'), $name)
, json_encode($console, JSON_PRETTY_PRINT)
sprintf('%s/%s.log', rtrim(static::$storeConsoleLogAt, '/'), $name), json_encode($console, JSON_PRETTY_PRINT)
);
}
}
@@ -269,9 +379,29 @@ class Browser
}
/**
* Switch to a specified frame in the browser.
* Store a snapshot of the page's current source code with the given name.
*
* @param string $name
* @return $this
*/
public function storeSource($name)
{
$source = $this->driver->getPageSource();
if (! empty($source)) {
file_put_contents(
sprintf('%s/%s.txt', rtrim(static::$storeSourceAt, '/'), $name), $source
);
}
return $this;
}
/**
* Switch to a specified frame in the browser and execute the given callback.
*
* @param string $selector
* @param \Closure $callback
* @return $this
*/
public function withinFrame($selector, Closure $callback)
@@ -311,7 +441,7 @@ class Browser
);
if ($this->page) {
$browser->on($this->page);
$browser->onWithoutAssert($this->page);
}
if ($selector instanceof Component) {
@@ -355,7 +485,7 @@ class Browser
*/
public function ensurejQueryIsAvailable()
{
if ($this->driver->executeScript("return window.jQuery == null")) {
if ($this->driver->executeScript('return window.jQuery == null')) {
$this->driver->executeScript(file_get_contents(__DIR__.'/../bin/jquery.js'));
}
}
@@ -439,6 +569,8 @@ class Browser
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{

View File

@@ -2,8 +2,8 @@
namespace Laravel\Dusk\Chrome;
use Laravel\Dusk\OperatingSystem;
use RuntimeException;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;
class ChromeProcess
@@ -11,7 +11,7 @@ class ChromeProcess
/**
* The path to the Chromedriver.
*
* @var String
* @var string
*/
protected $driver;
@@ -20,6 +20,8 @@ class ChromeProcess
*
* @param string $driver
* @return void
*
* @throws \RuntimeException
*/
public function __construct($driver = null)
{
@@ -33,37 +35,37 @@ class ChromeProcess
/**
* Build the process to run Chromedriver.
*
* @param array $arguments
* @return \Symfony\Component\Process\Process
*/
public function toProcess()
public function toProcess(array $arguments = [])
{
if ($this->driver) {
return $this->process();
return $this->process($arguments);
}
if ($this->onWindows()) {
$this->driver = realpath(__DIR__.'/../../bin/chromedriver-win.exe');
return $this->process();
} elseif ($this->onMac()) {
$this->driver = realpath(__DIR__.'/../../bin/chromedriver-mac');
} else {
$this->driver = realpath(__DIR__.'/../../bin/chromedriver-linux');
}
$this->driver = $this->onMac()
? realpath(__DIR__.'/../../bin/chromedriver-mac')
: realpath(__DIR__.'/../../bin/chromedriver-linux');
return $this->process();
return $this->process($arguments);
}
/**
* Build the Chromedriver with Symfony Process.
*
* @param array $arguments
* @return \Symfony\Component\Process\Process
*/
protected function process()
protected function process(array $arguments = [])
{
return (new Process(
[realpath($this->driver)], null, $this->chromeEnvironment()
));
return new Process(
array_merge([realpath($this->driver)], $arguments), null, $this->chromeEnvironment()
);
}
/**
@@ -77,7 +79,7 @@ class ChromeProcess
return [];
}
return ['DISPLAY' => ':0'];
return ['DISPLAY' => $_ENV['DISPLAY'] ?? ':0'];
}
/**
@@ -87,7 +89,7 @@ class ChromeProcess
*/
protected function onWindows()
{
return PHP_OS === 'WINNT' || Str::contains(php_uname(), 'Microsoft');
return OperatingSystem::onWindows();
}
/**
@@ -97,6 +99,6 @@ class ChromeProcess
*/
protected function onMac()
{
return PHP_OS === 'Darwin';
return OperatingSystem::onMac();
}
}

View File

@@ -21,13 +21,14 @@ trait SupportsChrome
/**
* Start the Chromedriver process.
*
* @throws \RuntimeException if the driver file path doesn't exist.
*
* @param array $arguments
* @return void
*
* @throws \RuntimeException
*/
public static function startChromeDriver()
public static function startChromeDriver(array $arguments = [])
{
static::$chromeProcess = static::buildChromeProcess();
static::$chromeProcess = static::buildChromeProcess($arguments);
static::$chromeProcess->start();
@@ -51,13 +52,14 @@ trait SupportsChrome
/**
* Build the process to run the Chromedriver.
*
*
* @param array $arguments
* @return \Symfony\Component\Process\Process
* @throws \RuntimeException if the driver file path doesn't exist.
*
* @throws \RuntimeException
*/
protected static function buildChromeProcess()
protected static function buildChromeProcess(array $arguments = [])
{
return (new ChromeProcess(static::$chromeDriver))->toProcess();
return (new ChromeProcess(static::$chromeDriver))->toProcess($arguments);
}
/**

View File

@@ -21,7 +21,7 @@ trait InteractsWithAuthentication
* Log into the application using a given user ID or email.
*
* @param object|string $userId
* @param string $guard
* @param string $guard
* @return $this
*/
public function loginAs($userId, $guard = null)

View File

@@ -17,12 +17,12 @@ trait InteractsWithCookies
*/
public function cookie($name, $value = null, $expiry = null, array $options = [])
{
if ($value) {
if (! is_null($value)) {
return $this->addCookie($name, $value, $expiry, $options);
}
if ($cookie = $this->driver->manage()->getCookieNamed($name)) {
return decrypt(rawurldecode($cookie['value']));
return decrypt(rawurldecode($cookie['value']), $unserialize = false);
}
}
@@ -37,7 +37,7 @@ trait InteractsWithCookies
*/
public function plainCookie($name, $value = null, $expiry = null, array $options = [])
{
if ($value) {
if (! is_null($value)) {
return $this->addCookie($name, $value, $expiry, $options, false);
}
@@ -59,7 +59,7 @@ trait InteractsWithCookies
public function addCookie($name, $value, $expiry = null, array $options = [], $encrypt = true)
{
if ($encrypt) {
$value = encrypt($value);
$value = encrypt($value, $serialize = false);
}
if ($expiry instanceof DateTimeInterface) {

View File

@@ -2,11 +2,11 @@
namespace Laravel\Dusk\Concerns;
use Illuminate\Support\Str;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\Remote\LocalFileDetector;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverKeys;
use Facebook\WebDriver\Remote\LocalFileDetector;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Illuminate\Support\Str;
trait InteractsWithElements
{
@@ -39,11 +39,11 @@ trait InteractsWithElements
* @param string $element
* @return $this
*/
public function clickLink($link, $element = "a")
public function clickLink($link, $element = 'a')
{
$this->ensurejQueryIsAvailable();
$selector = addslashes(trim($this->resolver->format("{$element}:contains({$link})")));
$selector = addslashes(trim($this->resolver->format("{$element}:contains({$link}):visible")));
$this->driver->executeScript("jQuery.find(\"{$selector}\")[0].click();");
@@ -66,7 +66,7 @@ trait InteractsWithElements
$selector = $this->resolver->format($selector);
$this->driver->executeScript(
"document.querySelector('{$selector}').value = '{$value}';"
'document.querySelector('.json_encode($selector).').value = '.json_encode($value).';'
);
return $this;
@@ -144,6 +144,21 @@ trait InteractsWithElements
return $this;
}
/**
* Type the given value in the given field slowly.
*
* @param string $field
* @param string $value
* @param int $pause
* @return $this
*/
public function typeSlowly($field, $value, $pause = 100)
{
$this->clear($field)->appendSlowly($field, $value, $pause);
return $this;
}
/**
* Type the given value in the given field without clearing it.
*
@@ -158,6 +173,23 @@ trait InteractsWithElements
return $this;
}
/**
* Type the given value in the given field slowly without clearing it.
*
* @param string $field
* @param string $value
* @param int $pause
* @return $this
*/
public function appendSlowly($field, $value, $pause = 100)
{
foreach (str_split($value) as $char) {
$this->append($field, $char)->pause($pause);
}
return $this;
}
/**
* Clear the given field.
*
@@ -182,11 +214,15 @@ trait InteractsWithElements
{
$element = $this->resolver->resolveForSelection($field);
$options = $element->findElements(WebDriverBy::tagName('option'));
$options = $element->findElements(WebDriverBy::cssSelector('option:not([disabled])'));
if (is_null($value)) {
$options[array_rand($options)]->click();
} else {
if (is_bool($value)) {
$value = $value ? '1' : '0';
}
foreach ($options as $option) {
if ((string) $option->getAttribute('value') === (string) $value) {
$option->click();

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Concerns;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\WebDriverBy;
trait InteractsWithMouse
{
@@ -54,6 +55,21 @@ trait InteractsWithMouse
return $this;
}
/**
* Click the element at the given XPath expression.
*
* @param string $selector
* @return $this
*/
public function clickAtXPath($expression)
{
$this->driver
->findElement(WebDriverBy::xpath($expression))
->click();
return $this;
}
/**
* Perform a mouse click and hold the mouse button down.
*

View File

@@ -2,10 +2,10 @@
namespace Laravel\Dusk\Concerns;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Illuminate\Support\Str;
use PHPUnit\Framework\Assert as PHPUnit;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\Exception\NoSuchElementException;
trait MakesAssertions
{
@@ -41,233 +41,6 @@ trait MakesAssertions
return $this;
}
/**
* Assert that the current URL matches the given URL.
*
* @param string $url
* @return $this
*/
public function assertUrlIs($url)
{
$pattern = str_replace('\*', '.*', preg_quote($url, '/'));
$segments = parse_url($this->driver->getCurrentURL());
$currentUrl = sprintf(
'%s://%s%s%s',
$segments['scheme'],
$segments['host'],
array_get($segments, 'port', '') ? ':'.$segments['port'] : '',
array_get($segments, 'path', '')
);
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $currentUrl,
"Actual URL [{$this->driver->getCurrentURL()}] does not equal expected URL [{$url}]."
);
return $this;
}
/**
* Assert that the current URL path matches the given pattern.
*
* @param string $path
* @return $this
*/
public function assertPathIs($path)
{
$pattern = preg_quote($path, '/');
$pattern = str_replace('\*', '.*', $pattern);
$actualPath = parse_url($this->driver->getCurrentURL())['path'];
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actualPath,
"Actual path [{$actualPath}] does not equal expected path [{$path}]."
);
return $this;
}
/**
* Assert that the current URL path begins with given path.
*
* @param string $path
* @return $this
*/
public function assertPathBeginsWith($path)
{
$actualPath = parse_url($this->driver->getCurrentURL())['path'];
PHPUnit::assertStringStartsWith(
$path, $actualPath,
"Actual path [{$actualPath}] does not begin with expected path [{$path}]."
);
return $this;
}
/**
* Assert that the current URL path does not match the given path.
*
* @param string $path
* @return $this
*/
public function assertPathIsNot($path)
{
$actualPath = parse_url($this->driver->getCurrentURL())['path'];
PHPUnit::assertNotEquals(
$path, $actualPath,
"Path [{$path}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL fragment matches the given pattern.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentIs($fragment)
{
$pattern = preg_quote($fragment, '/');
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertRegExp(
'/^'.str_replace('\*', '.*', $pattern).'$/u', $actualFragment,
"Actual fragment [{$actualFragment}] does not equal expected fragment [{$fragment}]."
);
return $this;
}
/**
* Assert that the current URL fragment begins with given fragment.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentBeginsWith($fragment)
{
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertStringStartsWith(
$fragment, $actualFragment,
"Actual fragment [$actualFragment] does not begin with expected fragment [$fragment]."
);
return $this;
}
/**
* Assert that the current URL fragment does not match the given fragment.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentIsNot($fragment)
{
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertNotEquals(
$fragment, $actualFragment,
"Fragment [{$fragment}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL path matches the given route.
*
* @param string $route
* @param array $parameters
* @return $this
*/
public function assertRouteIs($route, $parameters = [])
{
return $this->assertPathIs(route($route, $parameters, false));
}
/**
* Assert that a query string parameter is present and has a given value.
*
* @param string $name
* @param string $value
* @return $this
*/
public function assertQueryStringHas($name, $value = null)
{
$output = $this->assertHasQueryStringParameter($name);
if (is_null($value)) {
return $this;
}
PHPUnit::assertEquals(
$value, $output[$name],
"Query string parameter [{$name}] had value [{$output[$name]}], but expected [{$value}]."
);
return $this;
}
/**
* Assert that the given query string parameter is missing.
*
* @param string $name
* @return $this
*/
public function assertQueryStringMissing($name)
{
$parsedUrl = parse_url($this->driver->getCurrentURL());
if (! array_key_exists('query', $parsedUrl)) {
PHPUnit::assertTrue(true);
return $this;
}
parse_str($parsedUrl['query'], $output);
PHPUnit::assertArrayNotHasKey(
$name, $output,
"Found unexpected query string parameter [{$name}] in [".$this->driver->getCurrentURL()."]."
);
return $this;
}
/**
* Assert that the given query string parameter is present.
*
* @param string $name
* @return array
*/
protected function assertHasQueryStringParameter($name)
{
$parsedUrl = parse_url($this->driver->getCurrentURL());
PHPUnit::assertArrayHasKey(
'query', $parsedUrl,
"Did not see expected query string in [".$this->driver->getCurrentURL()."]."
);
parse_str($parsedUrl['query'], $output);
PHPUnit::assertArrayHasKey(
$name, $output,
"Did not see expected query string parameter [{$name}] in [".$this->driver->getCurrentURL()."]."
);
return $output;
}
/**
* Assert that the given cookie is present.
*
@@ -432,8 +205,8 @@ trait MakesAssertions
*/
public function assertSourceHas($code)
{
PHPUnit::assertContains(
$code, $this->driver->getPageSource(),
PHPUnit::assertTrue(
Str::contains($this->driver->getPageSource(), $code),
"Did not find expected source code [{$code}]"
);
@@ -448,8 +221,8 @@ trait MakesAssertions
*/
public function assertSourceMissing($code)
{
PHPUnit::assertNotContains(
$code, $this->driver->getPageSource(),
PHPUnit::assertFalse(
Str::contains($this->driver->getPageSource(), $code),
"Found unexpected source code [{$code}]"
);
@@ -682,8 +455,8 @@ JS;
/**
* Assert that the given array of values are available to be selected.
*
* @param string $field
* @param array $values
* @param string $field
* @param array $values
* @return $this
*/
public function assertSelectHasOptions($field, array $values)
@@ -696,7 +469,7 @@ JS;
PHPUnit::assertCount(
count($values), $options,
"Expected options [".implode(',', $values)."] for selection field [{$field}] to be available."
'Expected options ['.implode(',', $values)."] for selection field [{$field}] to be available."
);
return $this;
@@ -705,15 +478,15 @@ JS;
/**
* Assert that the given array of values are not available to be selected.
*
* @param string $field
* @param array $values
* @param string $field
* @param array $values
* @return $this
*/
public function assertSelectMissingOptions($field, array $values)
{
PHPUnit::assertCount(
0, $this->resolver->resolveSelectOptions($field, $values),
"Unexpected options [".implode(',', $values)."] for selection field [{$field}]."
'Unexpected options ['.implode(',', $values)."] for selection field [{$field}]."
);
return $this;
@@ -722,8 +495,8 @@ JS;
/**
* Assert that the given value is available to be selected on the given field.
*
* @param string $field
* @param string $value
* @param string $field
* @param string $value
* @return $this
*/
public function assertSelectHasOption($field, $value)
@@ -734,8 +507,8 @@ JS;
/**
* Assert that the given value is not available to be selected on the given field.
*
* @param string $field
* @param string $value
* @param string $field
* @param string $value
* @return $this
*/
public function assertSelectMissingOption($field, $value)
@@ -752,9 +525,11 @@ JS;
*/
public function selected($field, $value)
{
$element = $this->resolver->resolveForSelection($field);
$options = $this->resolver->resolveSelectOptions($field, (array) $value);
return (string) $element->getAttribute('value') === (string) $value;
return collect($options)->contains(function (RemoteWebElement $option) {
return $option->isSelected();
});
}
/**
@@ -773,6 +548,59 @@ JS;
return $this;
}
/**
* Assert that the element at the given selector has the given attribute value.
*
* @param string $selector
* @param string $attribute
* @param string $value
* @return $this
*/
public function assertAttribute($selector, $attribute, $value)
{
$fullSelector = $this->resolver->format($selector);
$actual = $this->resolver->findOrFail($selector)->getAttribute($attribute);
PHPUnit::assertNotNull(
$actual,
"Did not see expected attribute [{$attribute}] within element [{$fullSelector}]."
);
PHPUnit::assertEquals(
$value, $actual,
"Expected '$attribute' attribute [{$value}] does not equal actual value [$actual]."
);
return $this;
}
/**
* Assert that the element at the given selector has the given data attribute value.
*
* @param string $selector
* @param string $attribute
* @param string $value
* @return $this
*/
public function assertDataAttribute($selector, $attribute, $value)
{
return $this->assertAttribute($selector, 'data-'.$attribute, $value);
}
/**
* Assert that the element at the given selector has the given aria attribute value.
*
* @param string $selector
* @param string $attribute
* @param string $value
* @return $this
*/
public function assertAriaAttribute($selector, $attribute, $value)
{
return $this->assertAttribute($selector, 'aria-'.$attribute, $value);
}
/**
* Assert that the element with the given selector is visible.
*
@@ -854,7 +682,8 @@ JS;
* @param string $field
* @return $this
*/
public function assertEnabled($field) {
public function assertEnabled($field)
{
$element = $this->resolver->resolveForField($field);
PHPUnit::assertTrue(
@@ -871,7 +700,8 @@ JS;
* @param string $field
* @return $this
*/
public function assertDisabled($field) {
public function assertDisabled($field)
{
$element = $this->resolver->resolveForField($field);
PHPUnit::assertFalse(
@@ -882,13 +712,50 @@ JS;
return $this;
}
/**
* Assert that the given button is enabled.
*
* @param string $button
* @return $this
*/
public function assertButtonEnabled($button)
{
$element = $this->resolver->resolveForButtonPress($button);
PHPUnit::assertTrue(
$element->isEnabled(),
"Expected button [{$button}] to be enabled, but it wasn't."
);
return $this;
}
/**
* Assert that the given button is disabled.
*
* @param string $button
* @return $this
*/
public function assertButtonDisabled($button)
{
$element = $this->resolver->resolveForButtonPress($button);
PHPUnit::assertFalse(
$element->isEnabled(),
"Expected button [{$button}] to be disabled, but it wasn't."
);
return $this;
}
/**
* Assert that the given field is focused.
*
* @param string $field
* @return $this
*/
public function assertFocused($field) {
public function assertFocused($field)
{
$element = $this->resolver->resolveForField($field);
PHPUnit::assertTrue(
@@ -905,7 +772,8 @@ JS;
* @param string $field
* @return $this
*/
public function assertNotFocused($field) {
public function assertNotFocused($field)
{
$element = $this->resolver->resolveForField($field);
PHPUnit::assertFalse(
@@ -958,7 +826,10 @@ JS;
*/
public function assertVueContains($key, $value, $componentSelector = null)
{
PHPUnit::assertContains($value, $this->vueAttribute($componentSelector, $key));
$attribute = $this->vueAttribute($componentSelector, $key);
PHPUnit::assertIsArray($attribute, "The attribute for key [$key] is not an array.");
PHPUnit::assertContains($value, $attribute);
return $this;
}
@@ -974,7 +845,10 @@ JS;
*/
public function assertVueDoesNotContain($key, $value, $componentSelector = null)
{
PHPUnit::assertNotContains($value, $this->vueAttribute($componentSelector, $key));
$attribute = $this->vueAttribute($componentSelector, $key);
PHPUnit::assertIsArray($attribute, "The attribute for key [$key] is not an array.");
PHPUnit::assertNotContains($value, $attribute);
return $this;
}
@@ -991,7 +865,7 @@ JS;
$fullSelector = $this->resolver->format($componentSelector);
return $this->driver->executeScript(
"return document.querySelector('" . $fullSelector . "').__vue__." . $key
"return document.querySelector('".$fullSelector."').__vue__.".$key
);
}
}

View File

@@ -0,0 +1,353 @@
<?php
namespace Laravel\Dusk\Concerns;
use Illuminate\Support\Arr;
use PHPUnit\Framework\Assert as PHPUnit;
trait MakesUrlAssertions
{
/**
* Assert that the current URL matches the given URL.
*
* @param string $url
* @return $this
*/
public function assertUrlIs($url)
{
$pattern = str_replace('\*', '.*', preg_quote($url, '/'));
$segments = parse_url($this->driver->getCurrentURL());
$currentUrl = sprintf(
'%s://%s%s%s',
$segments['scheme'],
$segments['host'],
Arr::get($segments, 'port', '') ? ':'.$segments['port'] : '',
Arr::get($segments, 'path', '')
);
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $currentUrl,
"Actual URL [{$this->driver->getCurrentURL()}] does not equal expected URL [{$url}]."
);
return $this;
}
/**
* Assert that the current scheme matches the given scheme.
*
* @param string $scheme
* @return $this
*/
public function assertSchemeIs($scheme)
{
$pattern = str_replace('\*', '.*', preg_quote($scheme, '/'));
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_SCHEME) ?? '';
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actual,
"Actual scheme [{$actual}] does not equal expected scheme [{$pattern}]."
);
return $this;
}
/**
* Assert that the current scheme does not match the given scheme.
*
* @param string $scheme
* @return $this
*/
public function assertSchemeIsNot($scheme)
{
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_SCHEME) ?? '';
PHPUnit::assertNotEquals(
$scheme, $actual,
"Scheme [{$scheme}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current host matches the given host.
*
* @param string $host
* @return $this
*/
public function assertHostIs($host)
{
$pattern = str_replace('\*', '.*', preg_quote($host, '/'));
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_HOST) ?? '';
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actual,
"Actual host [{$actual}] does not equal expected host [{$pattern}]."
);
return $this;
}
/**
* Assert that the current host does not match the given host.
*
* @param string $host
* @return $this
*/
public function assertHostIsNot($host)
{
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_HOST) ?? '';
PHPUnit::assertNotEquals(
$host, $actual,
"Host [{$host}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current port matches the given port.
*
* @param string $port
* @return $this
*/
public function assertPortIs($port)
{
$pattern = str_replace('\*', '.*', preg_quote($port, '/'));
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_PORT) ?? '';
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actual,
"Actual port [{$actual}] does not equal expected port [{$pattern}]."
);
return $this;
}
/**
* Assert that the current host does not match the given host.
*
* @param string $port
* @return $this
*/
public function assertPortIsNot($port)
{
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_PORT) ?? '';
PHPUnit::assertNotEquals(
$port, $actual,
"Port [{$port}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL path matches the given pattern.
*
* @param string $path
* @return $this
*/
public function assertPathIs($path)
{
$pattern = str_replace('\*', '.*', preg_quote($path, '/'));
$actualPath = parse_url($this->driver->getCurrentURL(), PHP_URL_PATH) ?? '';
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actualPath,
"Actual path [{$actualPath}] does not equal expected path [{$path}]."
);
return $this;
}
/**
* Assert that the current URL path begins with given path.
*
* @param string $path
* @return $this
*/
public function assertPathBeginsWith($path)
{
$actualPath = parse_url($this->driver->getCurrentURL(), PHP_URL_PATH) ?? '';
PHPUnit::assertStringStartsWith(
$path, $actualPath,
"Actual path [{$actualPath}] does not begin with expected path [{$path}]."
);
return $this;
}
/**
* Assert that the current URL path does not match the given path.
*
* @param string $path
* @return $this
*/
public function assertPathIsNot($path)
{
$actualPath = parse_url($this->driver->getCurrentURL(), PHP_URL_PATH) ?? '';
PHPUnit::assertNotEquals(
$path, $actualPath,
"Path [{$path}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL fragment matches the given pattern.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentIs($fragment)
{
$pattern = preg_quote($fragment, '/');
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertRegExp(
'/^'.str_replace('\*', '.*', $pattern).'$/u', $actualFragment,
"Actual fragment [{$actualFragment}] does not equal expected fragment [{$fragment}]."
);
return $this;
}
/**
* Assert that the current URL fragment begins with given fragment.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentBeginsWith($fragment)
{
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertStringStartsWith(
$fragment, $actualFragment,
"Actual fragment [$actualFragment] does not begin with expected fragment [$fragment]."
);
return $this;
}
/**
* Assert that the current URL fragment does not match the given fragment.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentIsNot($fragment)
{
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertNotEquals(
$fragment, $actualFragment,
"Fragment [{$fragment}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL path matches the given route.
*
* @param string $route
* @param array $parameters
* @return $this
*/
public function assertRouteIs($route, $parameters = [])
{
return $this->assertPathIs(route($route, $parameters, false));
}
/**
* Assert that a query string parameter is present and has a given value.
*
* @param string $name
* @param string $value
* @return $this
*/
public function assertQueryStringHas($name, $value = null)
{
$output = $this->assertHasQueryStringParameter($name);
if (is_null($value)) {
return $this;
}
$parsedOutputName = is_array($output[$name]) ? implode(',', $output[$name]) : $output[$name];
$parsedValue = is_array($value) ? implode(',', $value) : $value;
PHPUnit::assertEquals(
$value, $output[$name],
"Query string parameter [{$name}] had value [{$parsedOutputName}], but expected [{$parsedValue}]."
);
return $this;
}
/**
* Assert that the given query string parameter is missing.
*
* @param string $name
* @return $this
*/
public function assertQueryStringMissing($name)
{
$parsedUrl = parse_url($this->driver->getCurrentURL());
if (! array_key_exists('query', $parsedUrl)) {
PHPUnit::assertTrue(true);
return $this;
}
parse_str($parsedUrl['query'], $output);
PHPUnit::assertArrayNotHasKey(
$name, $output,
"Found unexpected query string parameter [{$name}] in [".$this->driver->getCurrentURL().'].'
);
return $this;
}
/**
* Assert that the given query string parameter is present.
*
* @param string $name
* @return array
*/
protected function assertHasQueryStringParameter($name)
{
$parsedUrl = parse_url($this->driver->getCurrentURL());
PHPUnit::assertArrayHasKey(
'query', $parsedUrl,
'Did not see expected query string in ['.$this->driver->getCurrentURL().'].'
);
parse_str($parsedUrl['query'], $output);
PHPUnit::assertArrayHasKey(
$name, $output,
"Did not see expected query string parameter [{$name}] in [".$this->driver->getCurrentURL().'].'
);
return $output;
}
}

View File

@@ -4,10 +4,10 @@ namespace Laravel\Dusk\Concerns;
use Closure;
use Exception;
use Throwable;
use ReflectionFunction;
use Laravel\Dusk\Browser;
use Illuminate\Support\Collection;
use Laravel\Dusk\Browser;
use ReflectionFunction;
use Throwable;
trait ProvidesBrowser
{
@@ -56,6 +56,7 @@ trait ProvidesBrowser
*
* @param \Closure $callback
* @return \Laravel\Dusk\Browser|void
*
* @throws \Exception
* @throws \Throwable
*/
@@ -85,6 +86,7 @@ trait ProvidesBrowser
*
* @param \Closure $callback
* @return array
*
* @throws \ReflectionException
*/
protected function createBrowsersFor(Closure $callback)
@@ -118,6 +120,7 @@ trait ProvidesBrowser
*
* @param \Closure $callback
* @return int
*
* @throws \ReflectionException
*/
protected function browsersNeededFor(Closure $callback)
@@ -134,7 +137,11 @@ trait ProvidesBrowser
protected function captureFailuresFor($browsers)
{
$browsers->each(function ($browser, $key) {
$name = str_replace('\\', '_', get_class($this)).'_'.$this->getName(false);
if (property_exists($browser, 'fitOnFailure') && $browser->fitOnFailure) {
$browser->fitContent();
}
$name = $this->getCallerName();
$browser->screenshot('failure-'.$name.'-'.$key);
});
@@ -149,7 +156,7 @@ trait ProvidesBrowser
protected function storeConsoleLogsFor($browsers)
{
$browsers->each(function ($browser, $key) {
$name = str_replace('\\', '_', get_class($this)).'_'.$this->getName(false);
$name = $this->getCallerName();
$browser->storeConsoleLog($name.'-'.$key);
});
@@ -184,6 +191,7 @@ trait ProvidesBrowser
* Create the remote web driver instance.
*
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
*
* @throws \Exception
*/
protected function createWebDriver()
@@ -193,6 +201,16 @@ trait ProvidesBrowser
}, 50);
}
/**
* Get the browser caller name.
*
* @return string
*/
protected function getCallerName()
{
return str_replace('\\', '_', get_class($this)).'_'.$this->getName(false);
}
/**
* Create the RemoteWebDriver instance.
*

View File

@@ -2,13 +2,14 @@
namespace Laravel\Dusk\Concerns;
use Carbon\Carbon;
use Closure;
use Exception;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Facebook\WebDriver\Exception\TimeOutException;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Exception\TimeOutException;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
trait WaitsForElements
{
@@ -16,9 +17,10 @@ trait WaitsForElements
* Execute the given callback in a scoped browser once the selector is available.
*
* @param string $selector
* @param Closure $callback
* @param \Closure $callback
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function whenAvailable($selector, Closure $callback, $seconds = null)
@@ -32,13 +34,16 @@ trait WaitsForElements
* @param string $selector
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitFor($selector, $seconds = null)
{
$message = $this->formatTimeOutMessage('Waited %s seconds for selector', $selector);
return $this->waitUsing($seconds, 100, function () use ($selector) {
return $this->resolver->findOrFail($selector)->isDisplayed();
}, "Waited %s seconds for selector [{$selector}].");
}, $message);
}
/**
@@ -47,10 +52,13 @@ trait WaitsForElements
* @param string $selector
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitUntilMissing($selector, $seconds = null)
{
$message = $this->formatTimeOutMessage('Waited %s seconds for removal of selector', $selector);
return $this->waitUsing($seconds, 100, function () use ($selector) {
try {
$missing = ! $this->resolver->findOrFail($selector)->isDisplayed();
@@ -59,22 +67,47 @@ trait WaitsForElements
}
return $missing;
}, "Waited %s seconds for removal of selector [{$selector}].");
}, $message);
}
/**
* Wait for the given text to be removed.
*
* @param string $text
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitUntilMissingText($text, $seconds = null)
{
$text = Arr::wrap($text);
$message = $this->formatTimeOutMessage('Waited %s seconds for removal of text', implode("', '", $text));
return $this->waitUsing($seconds, 100, function () use ($text) {
return ! Str::contains($this->resolver->findOrFail('')->getText(), $text);
}, $message);
}
/**
* Wait for the given text to be visible.
*
* @param string $text
* @param array|string $text
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForText($text, $seconds = null)
{
$text = Arr::wrap($text);
$message = $this->formatTimeOutMessage('Waited %s seconds for text', implode("', '", $text));
return $this->waitUsing($seconds, 100, function () use ($text) {
return Str::contains($this->resolver->findOrFail('')->getText(), $text);
}, "Waited %s seconds for text [{$text}].");
}, $message);
}
/**
@@ -83,13 +116,16 @@ trait WaitsForElements
* @param string $link
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForLink($link, $seconds = null)
{
$message = $this->formatTimeOutMessage('Waited %s seconds for link', $link);
return $this->waitUsing($seconds, 100, function () use ($link) {
return $this->seeLink($link);
}, "Waited %s seconds for link [{$link}].");
}, $message);
}
/**
@@ -98,11 +134,14 @@ trait WaitsForElements
* @param string $path
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForLocation($path, $seconds = null)
{
return $this->waitUntil("window.location.pathname == '{$path}'", $seconds, "Waited %s seconds for location [{$path}].");
$message = $this->formatTimeOutMessage('Waited %s seconds for location', $path);
return $this->waitUntil("window.location.pathname == '{$path}'", $seconds, $message);
}
/**
@@ -112,6 +151,7 @@ trait WaitsForElements
* @param array $parameters
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForRoute($route, $parameters = [], $seconds = null)
@@ -126,6 +166,7 @@ trait WaitsForElements
* @param int $seconds
* @param string $message
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitUntil($script, $seconds = null, $message = null)
@@ -143,6 +184,40 @@ trait WaitsForElements
}, $message);
}
/**
* Wait until the Vue component's attribute at the given key has the given value.
*
* @param string $key
* @param string $value
* @param string|null $componentSelector
* @return $this
*/
public function waitUntilVue($key, $value, $componentSelector = null, $seconds = null)
{
$this->waitUsing($seconds, 100, function () use ($key, $value, $componentSelector) {
return $value == $this->vueAttribute($componentSelector, $key);
});
return $this;
}
/**
* Wait until the Vue component's attribute at the given key does not have the given value.
*
* @param string $key
* @param string $value
* @param string|null $componentSelector
* @return $this
*/
public function waitUntilVueIsNot($key, $value, $componentSelector = null, $seconds = null)
{
$this->waitUsing($seconds, 100, function () use ($key, $value, $componentSelector) {
return $value != $this->vueAttribute($componentSelector, $key);
});
return $this;
}
/**
* Wait for a JavaScript dialog to open.
*
@@ -163,14 +238,15 @@ trait WaitsForElements
/**
* Wait for the current page to reload.
*
* @param Closure $callback
* @param \Closure $callback
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForReload($callback = null, $seconds = null)
{
$token = str_random();
$token = Str::random();
$this->driver->executeScript("window['{$token}'] = {};");
@@ -188,9 +264,10 @@ trait WaitsForElements
*
* @param int $seconds
* @param int $interval
* @param Closure $callback
* @param \Closure $callback
* @param string|null $message
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitUsing($seconds, $interval, Closure $callback, $message = null)
@@ -222,4 +299,16 @@ trait WaitsForElements
return $this;
}
/**
* Prepare custom TimeOutException message for sprintf().
*
* @param string $message
* @param string $expected
* @return string
*/
protected function formatTimeOutMessage($message, $expected)
{
return $message.' ['.str_replace('%', '%%', $expected).'].';
}
}

View File

@@ -0,0 +1,251 @@
<?php
namespace Laravel\Dusk\Console;
use Illuminate\Console\Command;
use Laravel\Dusk\OperatingSystem;
use ZipArchive;
/**
* @copyright Originally created by Jonas Staudenmeir: https://github.com/staudenmeir/dusk-updater
*/
class ChromeDriverCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dusk:chrome-driver {version?}
{--all : Install a ChromeDriver binary for every OS}
{--proxy= : The proxy to download the binary through (example: "tcp://127.0.0.1:9000")}
{--ssl-no-verify : Bypass SSL certificate verification when installing through a proxy}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Install the ChromeDriver binary';
/**
* URL to the latest stable release version.
*
* @var string
*/
protected $latestVersionUrl = 'https://chromedriver.storage.googleapis.com/LATEST_RELEASE';
/**
* URL to the latest release version for a major Chrome version.
*
* @var string
*/
protected $versionUrl = 'https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%d';
/**
* URL to the ChromeDriver download.
*
* @var string
*/
protected $downloadUrl = 'https://chromedriver.storage.googleapis.com/%s/chromedriver_%s.zip';
/**
* Download slugs for the available operating systems.
*
* @var array
*/
protected $slugs = [
'linux' => 'linux64',
'mac' => 'mac64',
'win' => 'win32',
];
/**
* The legacy versions for the ChromeDriver.
*
* @var array
*/
protected $legacyVersions = [
43 => '2.20',
44 => '2.20',
45 => '2.20',
46 => '2.21',
47 => '2.21',
48 => '2.21',
49 => '2.22',
50 => '2.22',
51 => '2.23',
52 => '2.24',
53 => '2.26',
54 => '2.27',
55 => '2.28',
56 => '2.29',
57 => '2.29',
58 => '2.31',
59 => '2.32',
60 => '2.33',
61 => '2.34',
62 => '2.35',
63 => '2.36',
64 => '2.37',
65 => '2.38',
66 => '2.40',
67 => '2.41',
68 => '2.42',
69 => '2.44',
];
/**
* Path to the bin directory.
*
* @var string
*/
protected $directory = __DIR__.'/../../bin/';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$version = $this->version();
$all = $this->option('all');
$currentOS = OperatingSystem::id();
foreach ($this->slugs as $os => $slug) {
if ($all || ($os === $currentOS)) {
$archive = $this->download($version, $slug);
$binary = $this->extract($archive);
$this->rename($binary, $os);
}
}
$message = 'ChromeDriver %s successfully installed for version %s.';
$this->info(sprintf($message, $all ? 'binaries' : 'binary', $version));
}
/**
* Get the desired ChromeDriver version.
*
* @return string
*/
protected function version()
{
$version = $this->argument('version');
if (! $version) {
return $this->latestVersion();
}
if (! ctype_digit($version)) {
return $version;
}
$version = (int) $version;
if ($version < 70) {
return $this->legacyVersions[$version];
}
return trim($this->getUrl(
sprintf($this->versionUrl, $version)
));
}
/**
* Get the latest stable ChromeDriver version.
*
* @return string
*/
protected function latestVersion()
{
return trim(file_get_contents($this->latestVersionUrl));
}
/**
* Download the ChromeDriver archive.
*
* @param string $version
* @param string $slug
* @return string
*/
protected function download($version, $slug)
{
$url = sprintf($this->downloadUrl, $version, $slug);
file_put_contents(
$archive = $this->directory.'chromedriver.zip',
$this->getUrl($url)
);
return $archive;
}
/**
* Extract the ChromeDriver binary from the archive and delete the archive.
*
* @param string $archive
* @return string
*/
protected function extract($archive)
{
$zip = new ZipArchive;
$zip->open($archive);
$zip->extractTo($this->directory);
$binary = $zip->getNameIndex(0);
$zip->close();
unlink($archive);
return $binary;
}
/**
* Rename the ChromeDriver binary and make it executable.
*
* @param string $binary
* @param string $os
* @return void
*/
protected function rename($binary, $os)
{
$newName = str_replace('chromedriver', 'chromedriver-'.$os, $binary);
rename($this->directory.$binary, $this->directory.$newName);
chmod($this->directory.$newName, 0755);
}
/**
* Get the contents of a URL using the 'proxy' and 'ssl-no-verify' command options.
*
* @param string $url
* @return string|bool
*/
protected function getUrl(string $url)
{
$contextOptions = [];
if ($this->option('proxy')) {
$contextOptions['http'] = ['proxy' => $this->option('proxy'), 'request_fulluri' => true];
}
if ($this->option('ssl-no-verify')) {
$contextOptions['ssl'] = ['verify_peer' => false];
}
$streamContext = stream_context_create($contextOptions);
return file_get_contents($url, false, $streamContext);
}
}

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class ComponentCommand extends GeneratorCommand
{
@@ -45,7 +46,7 @@ class ComponentCommand extends GeneratorCommand
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/tests'.str_replace('\\', '/', $name).'.php';
}

View File

@@ -3,11 +3,12 @@
namespace Laravel\Dusk\Console;
use Dotenv\Dotenv;
use Illuminate\Support\Str;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;
class DuskCommand extends Command
{
@@ -28,7 +29,7 @@ class DuskCommand extends Command
/**
* Indicates if the project has its own PHPUnit configuration.
*
* @var boolean
* @var bool
*/
protected $hasPhpUnitConfiguration = false;
@@ -68,9 +69,15 @@ class DuskCommand extends Command
$this->output->writeln('Warning: '.$e->getMessage());
}
return $process->run(function ($type, $line) {
$this->output->write($line);
});
try {
return $process->run(function ($type, $line) {
$this->output->write($line);
});
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
}
});
}
@@ -81,6 +88,10 @@ class DuskCommand extends Command
*/
protected function binary()
{
if ('phpdbg' === PHP_SAPI) {
return [PHP_BINARY, '-qrr', 'vendor/phpunit/phpunit/phpunit'];
}
return [PHP_BINARY, 'vendor/phpunit/phpunit/phpunit'];
}
@@ -96,18 +107,28 @@ class DuskCommand extends Command
return ! Str::startsWith($option, '--env=');
}));
return array_merge(['-c', base_path('phpunit.dusk.xml')], $options);
if (! file_exists($file = base_path('phpunit.dusk.xml'))) {
$file = base_path('phpunit.dusk.xml.dist');
}
return array_merge(['-c', $file], $options);
}
/**
* Purge the failure screenshots
* Purge the failure screenshots.
*
* @return void
*/
protected function purgeScreenshots()
{
$path = base_path('tests/Browser/screenshots');
if (! is_dir($path)) {
return;
}
$files = Finder::create()->files()
->in(base_path('tests/Browser/screenshots'))
->in($path)
->name('failure-*');
foreach ($files as $file) {
@@ -122,8 +143,14 @@ class DuskCommand extends Command
*/
protected function purgeConsoleLogs()
{
$path = base_path('tests/Browser/console');
if (! is_dir($path)) {
return;
}
$files = Finder::create()->files()
->in(base_path('tests/Browser/console'))
->in($path)
->name('*.log');
foreach ($files as $file) {
@@ -138,6 +165,22 @@ class DuskCommand extends Command
* @return mixed
*/
protected function withDuskEnvironment($callback)
{
$this->setupDuskEnvironment();
try {
return $callback();
} finally {
$this->teardownDuskEnviroment();
}
}
/**
* Setup the Dusk environment.
*
* @return void
*/
protected function setupDuskEnvironment()
{
if (file_exists(base_path($this->duskFile()))) {
if (file_get_contents(base_path('.env')) !== file_get_contents(base_path($this->duskFile()))) {
@@ -149,13 +192,7 @@ class DuskCommand extends Command
$this->writeConfiguration();
return tap($callback(), function () {
$this->removeConfiguration();
if (file_exists(base_path($this->duskFile())) && file_exists(base_path('.env.backup'))) {
$this->restoreEnvironment();
}
});
$this->setupSignalHandler();
}
/**
@@ -170,18 +207,6 @@ class DuskCommand extends Command
copy(base_path($this->duskFile()), base_path('.env'));
}
/**
* Restore the backed-up environment file.
*
* @return void
*/
protected function restoreEnvironment()
{
copy(base_path('.env.backup'), base_path('.env'));
unlink(base_path('.env.backup'));
}
/**
* Refresh the current environment variables.
*
@@ -189,7 +214,21 @@ class DuskCommand extends Command
*/
protected function refreshEnvironment()
{
(new Dotenv(base_path()))->overload();
// BC fix to support Dotenv ^2.2...
if (! method_exists(Dotenv::class, 'create')) {
(new Dotenv(base_path()))->overload();
return;
}
// BC fix to support Dotenv ^3.0...
if (! method_exists(Dotenv::class, 'createMutable')) {
Dotenv::create(base_path())->overload();
return;
}
Dotenv::createMutable(base_path())->load();
}
/**
@@ -199,10 +238,43 @@ class DuskCommand extends Command
*/
protected function writeConfiguration()
{
if (! file_exists($file = base_path('phpunit.dusk.xml'))) {
if (! file_exists($file = base_path('phpunit.dusk.xml')) &&
! file_exists(base_path('phpunit.dusk.xml.dist'))) {
copy(realpath(__DIR__.'/../../stubs/phpunit.xml'), $file);
} else {
$this->hasPhpUnitConfiguration = true;
return;
}
$this->hasPhpUnitConfiguration = true;
}
/**
* Setup the SIGINT signal handler for CTRL+C exits.
*
* @return void
*/
protected function setupSignalHandler()
{
if (extension_loaded('pcntl')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, function () {
$this->teardownDuskEnviroment();
});
}
}
/**
* Restore the original environment.
*
* @return void
*/
protected function teardownDuskEnviroment()
{
$this->removeConfiguration();
if (file_exists(base_path($this->duskFile())) && file_exists(base_path('.env.backup'))) {
$this->restoreEnvironment();
}
}
@@ -213,11 +285,23 @@ class DuskCommand extends Command
*/
protected function removeConfiguration()
{
if (! $this->hasPhpUnitConfiguration) {
unlink(base_path('phpunit.dusk.xml'));
if (! $this->hasPhpUnitConfiguration && file_exists($file = base_path('phpunit.dusk.xml'))) {
unlink($file);
}
}
/**
* Restore the backed-up environment file.
*
* @return void
*/
protected function restoreEnvironment()
{
copy(base_path('.env.backup'), base_path('.env'));
unlink(base_path('.env.backup'));
}
/**
* Get the name of the Dusk file for the environment.
*

View File

@@ -0,0 +1,33 @@
<?php
namespace Laravel\Dusk\Console;
class DuskFailsCommand extends DuskCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dusk:fails {--without-tty : Disable output to TTY}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the failing Dusk tests from the last run and stop on failure';
/**
* Get the array of arguments for running PHPUnit.
*
* @param array $options
* @return array
*/
protected function phpunitArguments($options)
{
return array_unique(array_merge(parent::phpunitArguments($options), [
'--cache-result', '--order-by=defects', '--stop-on-failure',
]));
}
}

View File

@@ -11,7 +11,9 @@ class InstallCommand extends Command
*
* @var string
*/
protected $signature = 'dusk:install';
protected $signature = 'dusk:install
{--proxy= : The proxy to download the binary through (example: "tcp://127.0.0.1:9000")}
{--ssl-no-verify : Bypass SSL certificate verification when installing through a proxy}';
/**
* The console command description.
@@ -20,16 +22,6 @@ class InstallCommand extends Command
*/
protected $description = 'Install Dusk into the application';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
@@ -67,6 +59,20 @@ class InstallCommand extends Command
}
$this->info('Dusk scaffolding installed successfully.');
$this->comment('Downloading ChromeDriver binaries...');
$driverCommandArgs = ['--all' => true];
if ($this->option('proxy')) {
$driverCommandArgs['--proxy'] = $this->option('proxy');
}
if ($this->option('ssl-no-verify')) {
$driverCommandArgs['--ssl-no-verify'] = true;
}
$this->call('dusk:chrome-driver', $driverCommandArgs);
}
/**

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class MakeCommand extends GeneratorCommand
{
@@ -45,7 +46,7 @@ class MakeCommand extends GeneratorCommand
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/tests'.str_replace('\\', '/', $name).'.php';
}

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class PageCommand extends GeneratorCommand
{
@@ -45,7 +46,7 @@ class PageCommand extends GeneratorCommand
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/tests'.str_replace('\\', '/', $name).'.php';
}

View File

@@ -24,6 +24,8 @@ class Dusk
*
* @param array $options
* @return bool
*
* @throws \InvalidArgumentException
*/
protected static function duskEnvironment($options)
{
@@ -36,7 +38,7 @@ class Dusk
}
if (! is_array($options['environments'])) {
throw new InvalidArgumentException("Dusk environments must be listed as an array.");
throw new InvalidArgumentException('Dusk environments must be listed as an array.');
}
return app()->environment(...$options['environments']);

View File

@@ -2,7 +2,6 @@
namespace Laravel\Dusk;
use Exception;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
@@ -15,41 +14,42 @@ class DuskServiceProvider extends ServiceProvider
*/
public function boot()
{
Route::get('/_dusk/login/{userId}/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@login',
]);
if (! $this->app->environment('production')) {
Route::get('/_dusk/login/{userId}/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@login',
]);
Route::get('/_dusk/logout/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@logout',
]);
Route::get('/_dusk/logout/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@logout',
]);
Route::get('/_dusk/user/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@user',
]);
Route::get('/_dusk/user/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@user',
]);
}
}
/**
* Register any package services.
*
* @return void
* @throws Exception
*
* @throws \Exception
*/
public function register()
{
if ($this->app->environment('production')) {
throw new Exception('It is unsafe to run Dusk in production.');
}
if ($this->app->runningInConsole()) {
$this->commands([
Console\InstallCommand::class,
Console\DuskCommand::class,
Console\DuskFailsCommand::class,
Console\MakeCommand::class,
Console\PageCommand::class,
Console\ComponentCommand::class,
Console\ChromeDriverCommand::class,
]);
}
}

View File

@@ -3,10 +3,10 @@
namespace Laravel\Dusk;
use Exception;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
class ElementResolver
{
@@ -43,7 +43,7 @@ class ElementResolver
'findButtonBySelector',
'findButtonByName',
'findButtonByValue',
'findButtonByText'
'findButtonByText',
];
/**
@@ -77,6 +77,7 @@ class ElementResolver
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForTyping($field)
@@ -86,7 +87,7 @@ class ElementResolver
}
return $this->firstOrFail([
$field, "input[name='{$field}']", "textarea[name='{$field}']"
$field, "input[name='{$field}']", "textarea[name='{$field}']",
]);
}
@@ -95,6 +96,7 @@ class ElementResolver
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForSelection($field)
@@ -104,16 +106,17 @@ class ElementResolver
}
return $this->firstOrFail([
$field, "select[name='{$field}']"
$field, "select[name='{$field}']",
]);
}
/**
* Resolve all the options with the given value on the select field.
*
* @param string $field
* @param array $values
* @param string $field
* @param array $values
* @return \Facebook\WebDriver\Remote\RemoteWebElement[]
*
* @throws \Exception
*/
public function resolveSelectOptions($field, array $values)
@@ -136,7 +139,9 @@ class ElementResolver
* @param string $field
* @param string $value
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
* @throws \InvalidArgumentException
*/
public function resolveForRadioSelection($field, $value = null)
{
@@ -151,16 +156,17 @@ class ElementResolver
}
return $this->firstOrFail([
$field, "input[type=radio][name='{$field}'][value='{$value}']"
$field, "input[type=radio][name='{$field}'][value='{$value}']",
]);
}
/**
* Resolve the element for a given checkbox "field".
*
* @param string $field
* @param string|null $field
* @param string $value
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForChecking($field, $value = null)
@@ -169,14 +175,18 @@ class ElementResolver
return $element;
}
$selector = "input[type=checkbox][name='{$field}']";
$selector = 'input[type=checkbox]';
if (! is_null($field)) {
$selector .= "[name='{$field}']";
}
if (! is_null($value)) {
$selector .= "[value='{$value}']";
}
return $this->firstOrFail([
$field, $selector
$field, $selector,
]);
}
@@ -185,6 +195,7 @@ class ElementResolver
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForAttachment($field)
@@ -194,7 +205,7 @@ class ElementResolver
}
return $this->firstOrFail([
$field, "input[type=file][name='{$field}']"
$field, "input[type=file][name='{$field}']",
]);
}
@@ -203,6 +214,7 @@ class ElementResolver
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForField($field)
@@ -213,7 +225,7 @@ class ElementResolver
return $this->firstOrFail([
$field, "input[name='{$field}']", "textarea[name='{$field}']",
"select[name='{$field}']", "button[name='{$field}']"
"select[name='{$field}']", "button[name='{$field}']",
]);
}
@@ -222,6 +234,8 @@ class ElementResolver
*
* @param string $button
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \InvalidArgumentException
*/
public function resolveForButtonPress($button)
{
@@ -272,7 +286,7 @@ class ElementResolver
*/
protected function findButtonByValue($button)
{
foreach ($this->all("input[type=submit]") as $element) {
foreach ($this->all('input[type=submit]') as $element) {
if ($element->getAttribute('value') === $button) {
return $element;
}
@@ -327,6 +341,7 @@ class ElementResolver
*
* @param array $selectors
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function firstOrFail($selectors)
@@ -394,7 +409,7 @@ class ElementResolver
array_keys($sortedElements), array_values($sortedElements), $originalSelector = $selector
);
if (starts_with($selector, '@') && $selector === $originalSelector) {
if (Str::startsWith($selector, '@') && $selector === $originalSelector) {
$selector = '[dusk="'.explode('@', $selector)[1].'"]';
}

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
class UserController
{
@@ -31,6 +32,7 @@ class UserController
*
* @param string $userId
* @param string $guard
* @return void
*/
public function login($userId, $guard = null)
{
@@ -38,7 +40,7 @@ class UserController
$provider = Auth::guard($guard)->getProvider();
$user = str_contains($userId, '@')
$user = Str::contains($userId, '@')
? $provider->retrieveByCredentials(['email' => $userId])
: $provider->retrieveById($userId);
@@ -49,6 +51,7 @@ class UserController
* Log the user out of the application.
*
* @param string $guard
* @return void
*/
public function logout($guard = null)
{

View File

@@ -0,0 +1,38 @@
<?php
namespace Laravel\Dusk;
use Illuminate\Support\Str;
class OperatingSystem
{
/**
* Returns the current OS identifier.
*
* @return string
*/
public static function id()
{
return static::onWindows() ? 'win' : (static::onMac() ? 'mac' : 'linux');
}
/**
* Determine if the operating system is Windows or Windows Subsystem for Linux.
*
* @return bool
*/
public static function onWindows()
{
return PHP_OS === 'WINNT' || Str::contains(php_uname(), 'Microsoft');
}
/**
* Determine if the operating system is macOS.
*
* @return bool
*/
public static function onMac()
{
return PHP_OS === 'Darwin';
}
}

View File

@@ -3,22 +3,22 @@
namespace Laravel\Dusk;
use Exception;
use Laravel\Dusk\Chrome\SupportsChrome;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Illuminate\Foundation\Testing\TestCase as FoundationTestCase;
use Laravel\Dusk\Chrome\SupportsChrome;
use Laravel\Dusk\Concerns\ProvidesBrowser;
abstract class TestCase extends FoundationTestCase
{
use Concerns\ProvidesBrowser,
SupportsChrome;
use ProvidesBrowser, SupportsChrome;
/**
* Register the base URL with Dusk.
*
* @return void
*/
protected function setUp()
protected function setUp(): void
{
parent::setUp();
@@ -28,6 +28,8 @@ abstract class TestCase extends FoundationTestCase
Browser::$storeConsoleLogAt = base_path('tests/Browser/console');
Browser::$storeSourceAt = base_path('tests/Browser/source');
Browser::$userResolver = function () {
return $this->user();
};
@@ -59,10 +61,11 @@ abstract class TestCase extends FoundationTestCase
* Return the default user to authenticate.
*
* @return \App\User|int|null
*
* @throws \Exception
*/
protected function user()
{
throw new Exception("User resolver has not been set.");
throw new Exception('User resolver has not been set.');
}
}