Laravel version update

Laravel version update
This commit is contained in:
Manish Verma
2018-08-06 18:48:58 +05:30
parent d143048413
commit 126fbb0255
13678 changed files with 1031482 additions and 778530 deletions

43
vendor/laravel/dusk/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,43 @@
# Release Notes for 1.0.x
## v1.0.13 (2017-04-20)
### Added
- Added `illuminate/console` as dependency ([#232](https://github.com/laravel/dusk/pull/232))
- Added security measurement against registering Dusk on production ([#229](https://github.com/laravel/dusk/pull/229))
- Added `PHP_BINARY` constant to the list of PHP's executable binaries ([#240](https://github.com/laravel/dusk/pull/240))
### Changed
- Changed `propagateScaffoldingToBrowser()` to `setUp()` for compatibility with PHPUnit ~6.0 ([#227](https://github.com/laravel/dusk/pull/227))
- Changed `selected()` comparison to always cast the value to string ([#239](https://github.com/laravel/dusk/pull/239))
### Fixed
- No longer throws exception when Tty is not available ([#226](https://github.com/laravel/dusk/pull/226))
- Use `getAttribute('value')` instead of `getText()` for `textarea` elements ([#237](https://github.com/laravel/dusk/pull/237))
- Fixed bug when giving strings with apostrophe to `clickLink()` ([#228](https://github.com/laravel/dusk/pull/228))
## v1.0.12 (2017-04-07)
### Added
- Added automated tests for HTML elements identified by strings with a colon ([#214](https://github.com/laravel/dusk/pull/214))
### Fixed
- Support for colon on HTML id tag ([#214](https://github.com/laravel/dusk/pull/214))
## v1.0.11 (2017-03-20)
### Added
- Added `assertSelectHasOptions()`, `assertSelectMissingOptions()`, `assertSelectHasOption()` and `and assertSelectMissingOption()` ([#195](https://github.com/laravel/dusk/pull/195))
- Added purge console logs before starting tests ([#193](https://github.com/laravel/dusk/pull/193))
- Added `assertPathIsNot()` ([#183](https://github.com/laravel/dusk/pull/183))
- Added support for back button ([#187](https://github.com/laravel/dusk/pull/187))
- Added `waitForLocation()` to allow waiting on `window.location` to be changed ([#176](https://github.com/laravel/dusk/pull/176))
### Changed
- Updated ChromeDriver to v2.28 so that it works with Chrome 57 ([#199](https://github.com/laravel/dusk/pull/199))
- Comparison to `option` inside `select` will no longer be strict ([#178](https://github.com/laravel/dusk/pull/178))
- Type-hint Browser for auto-complete support ([#174](https://github.com/laravel/dusk/pull/174))
## v1.0.10 (2017-03-03)

21
vendor/laravel/dusk/LICENSE.md vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Binary file not shown.

BIN
vendor/laravel/dusk/bin/chromedriver-mac vendored Normal file

Binary file not shown.

Binary file not shown.

4
vendor/laravel/dusk/bin/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

41
vendor/laravel/dusk/composer.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "laravel/dusk",
"description": "Laravel Dusk provides simple end-to-end testing and browser automation.",
"keywords": ["laravel", "testing", "webdriver"],
"license": "MIT",
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": ">=5.6.4",
"facebook/webdriver": "~1.0",
"nesbot/carbon": "~1.20",
"illuminate/console": "~5.5",
"illuminate/support": "~5.5",
"symfony/console": "~3.2",
"symfony/process": "~3.2"
},
"require-dev": {
"phpunit/phpunit": "^5.7",
"mockery/mockery": "^0.9.6"
},
"autoload": {
"psr-4": {
"Laravel\\Dusk\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
},
"laravel": {
"providers": [
"Laravel\\Dusk\\DuskServiceProvider"
]
}
},
"minimum-stability": "dev"
}

26
vendor/laravel/dusk/phpunit.xml.dist vendored Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
verbose="true"
>
<testsuites>
<testsuite name="Laravel Dusk Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>

20
vendor/laravel/dusk/readme.md vendored Normal file
View File

@@ -0,0 +1,20 @@
<p align="center"><img src="https://laravel.com/assets/img/components/logo-dusk.svg"></p>
<p align="center">
<a href="https://travis-ci.org/laravel/dusk"><img src="https://travis-ci.org/laravel/dusk.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/dusk"><img src="https://poser.pugx.org/laravel/dusk/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/dusk"><img src="https://poser.pugx.org/laravel/dusk/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/dusk"><img src="https://poser.pugx.org/laravel/dusk/license.svg" alt="License"></a>
</p>
## Introduction
Laravel Dusk provides an expressive, easy-to-use browser automation and testing API. By default, Dusk does not require you to install JDK or Selenium on your machine. Instead, Dusk uses a standalone Chromedriver. However, you are free to utilize any other Selenium driver you wish.
## Official Documentation
Documentation for Dusk can be found on the [Laravel website](https://laravel.com/docs/master/dusk).
## License
Laravel Dusk is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)

386
vendor/laravel/dusk/src/Browser.php vendored Normal file
View File

@@ -0,0 +1,386 @@
<?php
namespace Laravel\Dusk;
use Closure;
use BadMethodCallException;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Facebook\WebDriver\WebDriverDimension;
class Browser
{
use Concerns\InteractsWithAuthentication,
Concerns\InteractsWithCookies,
Concerns\InteractsWithElements,
Concerns\InteractsWithJavascript,
Concerns\InteractsWithMouse,
Concerns\MakesAssertions,
Concerns\WaitsForElements,
Macroable {
__call as macroCall;
}
/**
* The base URL for all URLs.
*
* @var string
*/
public static $baseUrl;
/**
* The directory that will contain any screenshots.
*
* @var string
*/
public static $storeScreenshotsAt;
/**
* The directory that will contain any console logs.
*
* @var string
*/
public static $storeConsoleLogAt;
/**
* Get the callback which resolves the default user to authenticate.
*
* @var \Closure
*/
public static $userResolver;
/**
* The RemoteWebDriver instance.
*
* @var \Facebook\WebDriver\Remote\RemoteWebDriver
*/
public $driver;
/**
* The element resolver instance.
*
* @var ElementResolver
*/
public $resolver;
/**
* The page object currently being viewed.
*
* @var mixed
*/
public $page;
/**
* 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 ElementResolver($driver);
}
/**
* Browse to the given URL.
*
* @param string $url
* @return $this
*/
public function visit($url)
{
// First, if the URL is an object it means we are actually dealing with a page
// and we need to create this page then get the URL from the page object as
// it contains the URL. Once that is done, we will be ready to format it.
if (is_object($url)) {
$page = $url;
$url = $page->url();
}
// If the URL does not start with http or https, then we will prepend the base
// URL onto the URL and navigate to the URL. This will actually navigate to
// the URL in the browser. Then we will be ready to make assertions, etc.
if (! Str::startsWith($url, ['http://', 'https://'])) {
$url = static::$baseUrl.'/'.ltrim($url, '/');
}
$this->driver->navigate()->to($url);
// If the page variable was set, we will call the "on" method which will set a
// page instance variable and call an assert method on the page so that the
// page can have the chance to verify that we are within the right pages.
if (isset($page)) {
$this->on($page);
}
return $this;
}
/**
* Browse to the given route.
*
* @param string $route
* @param array $parameters
* @return $this
*/
public function visitRoute($route, $parameters = [])
{
return $this->visit(route($route, $parameters));
}
/**
* Set the current page object.
*
* @param mixed $page
* @return $this
*/
public function on($page)
{
$this->page = $page;
// Here we will set the page elements on the resolver instance, which will allow
// the developer to access short-cuts for CSS selectors on the page which can
// allow for more expressive navigation and interaction with all the pages.
$this->resolver->pageElements(array_merge(
$page::siteElements(), $page->elements()
));
$page->assert($this);
return $this;
}
/**
* Refresh the page.
*
* @return $this
*/
public function refresh()
{
$this->driver->navigate()->refresh();
return $this;
}
/**
* Navigate to the previous page.
*
* @return $this
*/
public function back()
{
$this->driver->navigate()->back();
return $this;
}
/**
* Maximize the browser window.
*
* @return $this
*/
public function maximize()
{
$this->driver->manage()->window()->maximize();
return $this;
}
/**
* Resize the browser window.
*
* @param int $width
* @param int $height
* @return $this
*/
public function resize($width, $height)
{
$this->driver->manage()->window()->setSize(
new WebDriverDimension($width, $height)
);
return $this;
}
/**
* Take a screenshot and store it with the given name.
*
* @param string $name
* @return $this
*/
public function screenshot($name)
{
$this->driver->takeScreenshot(
sprintf('%s/%s.png', rtrim(static::$storeScreenshotsAt, '/'), $name)
);
return $this;
}
/**
* Store the console output with the given name.
*
* @param string $name
* @return $this
*/
public function storeConsoleLog($name)
{
$console = $this->driver->manage()->getLog('browser');
if (! empty($console)) {
file_put_contents(
sprintf('%s/%s.log', rtrim(static::$storeConsoleLogAt, '/'), $name)
, json_encode($console, JSON_PRETTY_PRINT)
);
}
return $this;
}
/**
* Execute a Closure with a scoped browser instance.
*
* @param string $selector
* @param \Closure $callback
* @return $this
*/
public function within($selector, Closure $callback)
{
return $this->with($selector, $callback);
}
/**
* Execute a Closure with a scoped browser instance.
*
* @param string $selector
* @param \Closure $callback
* @return $this
*/
public function with($selector, Closure $callback)
{
$browser = new static(
$this->driver, new ElementResolver($this->driver, $this->resolver->format($selector))
);
if ($this->page) {
$browser->on($this->page);
}
call_user_func($callback, $browser);
return $this;
}
/**
* Ensure that jQuery is available on the page.
*
* @return void
*/
public function ensurejQueryIsAvailable()
{
if ($this->driver->executeScript("return window.jQuery == null")) {
$this->driver->executeScript(file_get_contents(__DIR__.'/../bin/jquery.js'));
}
}
/**
* Pause for the given amount of milliseconds.
*
* @param int $milliseconds
* @return $this
*/
public function pause($milliseconds)
{
usleep($milliseconds * 1000);
return $this;
}
/**
* Close the browser.
*
* @return void
*/
public function quit()
{
$this->driver->quit();
}
/**
* Tap the browser into a callback.
*
* @param \Closure $callback
* @return $this
*/
public function tap($callback)
{
$callback($this);
return $this;
}
/**
* Dump the content from the last response.
*
* @return void
*/
public function dump()
{
dd($this->driver->getPageSource());
}
/**
* Pause execution of test and open Laravel Tinker (PsySH) REPL.
*
* @return $this
*/
public function tinker()
{
\Psy\Shell::debug([
'browser' => $this,
'driver' => $this->driver,
'resolver' => $this->resolver,
'page' => $this->page,
], $this);
return $this;
}
/**
* Stop running tests but leave the browser open.
*
* @return void
*/
public function stop()
{
exit();
}
/**
* Dynamically call a method on the browser.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
if ($this->page && method_exists($this->page, $method)) {
array_unshift($parameters, $this);
$this->page->{$method}(...$parameters);
return $this;
}
throw new BadMethodCallException("Call to undefined method [{$method}].");
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Laravel\Dusk\Concerns;
use Laravel\Dusk\Browser;
use PHPUnit\Framework\Assert as PHPUnit;
trait InteractsWithAuthentication
{
/**
* Log into the application as the default user.
*
* @return $this
*/
public function login()
{
return $this->loginAs(call_user_func(Browser::$userResolver));
}
/**
* Log into the application using a given user ID or email.
*
* @param object|string $userId
* @param string $guard
* @return $this
*/
public function loginAs($userId, $guard = null)
{
$userId = method_exists($userId, 'getKey') ? $userId->getKey() : $userId;
return $this->visit(rtrim('/_dusk/login/'.$userId.'/'.$guard, '/'));
}
/**
* Log out of the application.
*
* @param string $guard
* @return $this
*/
public function logout($guard = null)
{
return $this->visit(rtrim('/_dusk/logout/'.$guard, '/'));
}
/**
* Get the ID and the class name of the authenticated user.
*
* @param string|null $guard
* @return array
*/
protected function currentUserInfo($guard = null)
{
$response = $this->visit("/_dusk/user/{$guard}");
return json_decode(strip_tags($response->driver->getPageSource()), true);
}
/**
* Assert that the user is authenticated.
*
* @param string|null $guard
* @return $this
*/
public function assertAuthenticated($guard = null)
{
PHPUnit::assertNotEmpty($this->currentUserInfo($guard), 'The user is not authenticated.');
return $this;
}
/**
* Assert that the user is not authenticated.
*
* @param string|null $guard
* @return $this
*/
public function assertGuest($guard = null)
{
PHPUnit::assertEmpty(
$this->currentUserInfo($guard), 'The user is unexpectedly authenticated.'
);
return $this;
}
/**
* Assert that the user is authenticated as the given user.
*
* @param $user
* @param string|null $guard
* @return $this
*/
public function assertAuthenticatedAs($user, $guard = null)
{
$expected = [
'id' => $user->getAuthIdentifier(),
'className' => get_class($user),
];
PHPUnit::assertSame(
$expected, $this->currentUserInfo($guard),
'The currently authenticated user is not who was expected.'
);
return $this;
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Laravel\Dusk\Concerns;
use DateTimeInterface;
trait InteractsWithCookies
{
/**
* Get or set an encrypted cookie's value.
*
* @param string $name
* @param string|null $value
* @param int|DateTimeInterface|null $expiry
* @param array $options
* @return string
*/
public function cookie($name, $value = null, $expiry = null, array $options = [])
{
if ($value) {
return $this->addCookie($name, $value, $expiry, $options);
}
if ($cookie = $this->driver->manage()->getCookieNamed($name)) {
return decrypt(rawurldecode($cookie['value']));
}
}
/**
* Get or set a plain cookie's value.
*
* @param string $name
* @param string|null $value
* @param int|DateTimeInterface|null $expiry
* @param array $options
* @return string
*/
public function plainCookie($name, $value = null, $expiry = null, array $options = [])
{
if ($value) {
return $this->addCookie($name, $value, $expiry, $options, false);
}
if ($cookie = $this->driver->manage()->getCookieNamed($name)) {
return rawurldecode($cookie['value']);
}
}
/**
* Add the given cookie.
*
* @param string $name
* @param string $value
* @param int|DateTimeInterface|null $expiry
* @param array $options
* @param bool $encrypt
* @return $this
*/
public function addCookie($name, $value, $expiry = null, array $options = [], $encrypt = true)
{
if ($encrypt) {
$value = encrypt($value);
}
if ($expiry instanceof DateTimeInterface) {
$expiry = $expiry->getTimestamp();
}
$this->driver->manage()->addCookie(
array_merge($options, compact('expiry', 'name', 'value'))
);
return $this;
}
/**
* Delete the given cookie.
*
* @param string $name
* @return $this
*/
public function deleteCookie($name)
{
$this->driver->manage()->deleteCookieNamed($name);
return $this;
}
}

View File

@@ -0,0 +1,432 @@
<?php
namespace Laravel\Dusk\Concerns;
use Illuminate\Support\Str;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverKeys;
use Facebook\WebDriver\Remote\LocalFileDetector;
use Facebook\WebDriver\Interactions\WebDriverActions;
trait InteractsWithElements
{
/**
* Get all of the elements matching the given selector.
*
* @param string $selector
* @return array
*/
public function elements($selector)
{
return $this->resolver->all($selector);
}
/**
* Get the element matching the given selector.
*
* @param string $selector
* @return \Facebook\WebDriver\Remote\RemoteWebElement|null
*/
public function element($selector)
{
return $this->resolver->find($selector);
}
/**
* Click the element at the given selector.
*
* @param string $selector
* @return $this
*/
public function click($selector)
{
$this->resolver->findOrFail($selector)->click();
return $this;
}
/**
* Right click the element at the given selector.
*
* @param string $selector
* @return $this
*/
public function rightClick($selector)
{
(new WebDriverActions($this->driver))->contextClick(
$this->resolver->findOrFail($selector)
)->perform();
return $this;
}
/**
* Click the link with the given text.
*
* @param string $link
* @return $this
*/
public function clickLink($link)
{
$this->ensurejQueryIsAvailable();
$selector = addslashes(trim($this->resolver->format("a:contains({$link})")));
$this->driver->executeScript("jQuery.find(\"{$selector}\")[0].click();");
return $this;
}
/**
* Directly get or set the value attribute of an input field.
*
* @param string $selector
* @param string|null $value
* @return $this
*/
public function value($selector, $value = null)
{
if (is_null($value)) {
return $this->resolver->findOrFail($selector)->getAttribute('value');
}
$selector = $this->resolver->format($selector);
$this->driver->executeScript(
"document.querySelector('{$selector}').value = '{$value}';"
);
return $this;
}
/**
* Get the text of the element matching the given selector.
*
* @param string $selector
* @return string
*/
public function text($selector)
{
return $this->resolver->findOrFail($selector)->getText();
}
/**
* Get the given attribute from the element matching the given selector.
*
* @param string $selector
* @param string $attribute
* @return string
*/
public function attribute($selector, $attribute)
{
return $this->resolver->findOrFail($selector)->getAttribute($attribute);
}
/**
* Send the given keys to the element matching the given selector.
*
* @param string $selector
* @param dynamic $keys
* @return $this
*/
public function keys($selector, ...$keys)
{
$this->resolver->findOrFail($selector)->sendKeys($this->parseKeys($keys));
return $this;
}
/**
* Parse the keys before sending to the keyboard.
*
* @param array $keys
* @return array
*/
protected function parseKeys($keys)
{
return collect($keys)->map(function ($key) {
if (is_string($key) && Str::startsWith($key, '{') && Str::endsWith($key, '}')) {
$key = constant(WebDriverKeys::class.'::'.strtoupper(trim($key, '{}')));
}
if (is_array($key) && Str::startsWith($key[0], '{')) {
$key[0] = constant(WebDriverKeys::class.'::'.strtoupper(trim($key[0], '{}')));
}
return $key;
})->all();
}
/**
* Type the given value in the given field.
*
* @param string $field
* @param string $value
* @return $this
*/
public function type($field, $value)
{
$this->resolver->resolveForTyping($field)->clear()->sendKeys($value);
return $this;
}
/**
* Type the given value in the given field without clearing it.
*
* @param string $field
* @param string $value
* @return $this
*/
public function append($field, $value)
{
$this->resolver->resolveForTyping($field)->sendKeys($value);
return $this;
}
/**
* Clear the given field.
*
* @param string $field
* @return $this
*/
public function clear($field)
{
$this->resolver->resolveForTyping($field)->clear();
return $this;
}
/**
* Select the given value or random value of a drop-down field.
*
* @param string $field
* @param string $value
* @return $this
*/
public function select($field, $value = null)
{
$element = $this->resolver->resolveForSelection($field);
$options = $element->findElements(WebDriverBy::tagName('option'));
if (is_null($value)) {
$options[array_rand($options)]->click();
}
else {
foreach ($options as $option) {
if ((string) $option->getAttribute('value') === (string) $value) {
$option->click();
break;
}
}
}
return $this;
}
/**
* Select the given value of a radio button field.
*
* @param string $field
* @param string $value
* @return $this
*/
public function radio($field, $value)
{
$this->resolver->resolveForRadioSelection($field, $value)->click();
return $this;
}
/**
* Check the given checkbox.
*
* @param string $field
* @param string $value
* @return $this
*/
public function check($field, $value = null)
{
$element = $this->resolver->resolveForChecking($field, $value);
if (! $element->isSelected()) {
$element->click();
}
return $this;
}
/**
* Uncheck the given checkbox.
*
* @param string $field
* @param string $value
* @return $this
*/
public function uncheck($field, $value = null)
{
$element = $this->resolver->resolveForChecking($field, $value);
if ($element->isSelected()) {
$element->click();
}
return $this;
}
/**
* Attach the given file to the field.
*
* @param string $field
* @param string $path
* @return $this
*/
public function attach($field, $path)
{
$element = $this->resolver->resolveForAttachment($field);
$element->setFileDetector(new LocalFileDetector)->sendKeys($path);
return $this;
}
/**
* Press the button with the given text or name.
*
* @param string $button
* @return $this
*/
public function press($button)
{
$this->resolver->resolveForButtonPress($button)->click();
return $this;
}
/**
* Press the button with the given text or name.
*
* @param string $button
* @param int $seconds
* @return $this
*/
public function pressAndWaitFor($button, $seconds = 5)
{
$element = $this->resolver->resolveForButtonPress($button);
$element->click();
return $this->waitUsing($seconds, 100, function () use ($element) {
return $element->isEnabled();
});
}
/**
* Drag an element to another element using selectors.
*
* @param string $from
* @param string $to
* @return $this
*/
public function drag($from, $to)
{
(new WebDriverActions($this->driver))->dragAndDrop(
$this->resolver->findOrFail($from), $this->resolver->findOrFail($to)
)->perform();
return $this;
}
/**
* Drag an element up.
*
* @param string $selector
* @param int $offset
* @return $this
*/
public function dragUp($selector, $offset)
{
return $this->dragOffset($selector, 0, -$offset);
}
/**
* Drag an element down.
*
* @param string $selector
* @param int $offset
* @return $this
*/
public function dragDown($selector, $offset)
{
return $this->dragOffset($selector, 0, $offset);
}
/**
* Drag an element to the left.
*
* @param string $selector
* @param int $offset
* @return $this
*/
public function dragLeft($selector, $offset)
{
return $this->dragOffset($selector, -$offset, 0);
}
/**
* Drag an element to the right.
*
* @param string $selector
* @param int $offset
* @return $this
*/
public function dragRight($selector, $offset)
{
return $this->dragOffset($selector, $offset, 0);
}
/**
* Drag an element by the given offset.
*
* @param string $selector
* @param int $x
* @param int $y
* @return $this
*/
public function dragOffset($selector, $x = 0, $y = 0)
{
(new WebDriverActions($this->driver))->dragAndDropBy(
$this->resolver->findOrFail($selector), $x, $y
)->perform();
return $this;
}
/**
* Accept a JavaScript dialog.
*
* @return $this
*/
public function acceptDialog()
{
$this->driver->switchTo()->alert()->accept();
return $this;
}
/**
* Dismiss a JavaScript dialog.
*
* @return $this
*/
public function dismissDialog()
{
$this->driver->switchTo()->alert()->dismiss();
return $this;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Laravel\Dusk\Concerns;
trait InteractsWithJavascript
{
/**
* Execute JavaScript within the browser.
*
* @param string|array $scripts
* @return array
*/
public function script($scripts)
{
return collect((array) $scripts)->map(function ($script) {
return $this->driver->executeScript($script);
})->all();
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Laravel\Dusk\Concerns;
trait InteractsWithMouse
{
/**
* Move the mouse over the given selector.
*
* @param string $selector
* @return $this
*/
public function mouseover($selector)
{
$element = $this->resolver->findOrFail($selector);
$this->driver->getMouse()->mouseMove($element->getCoordinates());
return $this;
}
}

View File

@@ -0,0 +1,676 @@
<?php
namespace Laravel\Dusk\Concerns;
use Illuminate\Support\Str;
use PHPUnit\Framework\Assert as PHPUnit;
use Facebook\WebDriver\Exception\NoSuchElementException;
trait MakesAssertions
{
/**
* Assert that the page title is the given value.
*
* @param string $title
* @return $this
*/
public function assertTitle($title)
{
PHPUnit::assertEquals($title, $this->driver->getTitle());
return $this;
}
/**
* Assert that the page title contains the given value.
*
* @param string $title
* @return $this
*/
public function assertTitleContains($title)
{
PHPUnit::assertTrue(
Str::contains($this->driver->getTitle(), $title)
);
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);
PHPUnit::assertRegExp('/^'.$pattern.'/u', parse_url(
$this->driver->getCurrentURL()
)['path']);
return $this;
}
/**
* Assert that the current URL path begins with given path.
*
* @param string $path
* @return $this
*/
public function assertPathBeginsWith($path)
{
PHPUnit::assertStringStartsWith($path, parse_url(
$this->driver->getCurrentURL()
)['path']);
return $this;
}
/**
* Assert that the current URL path does not match the given path.
*
* @param string $path
* @return $this
*/
public function assertPathIsNot($path)
{
PHPUnit::assertNotEquals($path, parse_url(
$this->driver->getCurrentURL()
)['path']);
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 $this
*/
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.
*
* @param string $name
* @return $this
*/
public function assertHasCookie($name)
{
PHPUnit::assertTrue(
! is_null($this->cookie($name)),
"Did not find expected cookie [{$name}]."
);
return $this;
}
/**
* Assert that an encrypted cookie has a given value.
*
* @param string $name
* @param string $value
* @param bool $decrypt
* @return $this
*/
public function assertCookieValue($name, $value, $decrypt = true)
{
$actual = $decrypt ? $this->cookie($name) : $this->plainCookie($name);
PHPUnit::assertEquals(
$value, $actual,
"Cookie [{$name}] had value [{$actual}], but expected [{$value}]."
);
return $this;
}
/**
* Assert that a cookie has a given value.
*
* @param string $name
* @param string $value
* @return $this
*/
public function assertPlainCookieValue($name, $value)
{
return $this->assertCookieValue($name, $value, false);
}
/**
* Assert that the given text appears on the page.
*
* @param string $text
* @return $this
*/
public function assertSee($text)
{
return $this->assertSeeIn('', $text);
}
/**
* Assert that the given text does not appear on the page.
*
* @param string $text
* @return $this
*/
public function assertDontSee($text)
{
return $this->assertDontSeeIn('', $text);
}
/**
* Assert that the given text appears within the given selector.
*
* @param string $selector
* @param string $text
* @return $this
*/
public function assertSeeIn($selector, $text)
{
$fullSelector = $this->resolver->format($selector);
$element = $this->resolver->findOrFail($selector);
PHPUnit::assertTrue(
Str::contains($element->getText(), $text),
"Did not see expected text [{$text}] within element [{$fullSelector}]."
);
return $this;
}
/**
* Assert that the given text does not appear within the given selector.
*
* @param string $selector
* @param string $text
* @return $this
*/
public function assertDontSeeIn($selector, $text)
{
$fullSelector = $this->resolver->format($selector);
$element = $this->resolver->findOrFail($selector);
PHPUnit::assertFalse(
Str::contains($element->getText(), $text),
"Saw unexpected text [{$text}] within element [{$fullSelector}]."
);
return $this;
}
/**
* Assert that the given source code is present on the page.
*
* @param string $code
* @return $this
*/
public function assertSourceHas($code)
{
PHPUnit::assertContains(
$code, $this->driver->getPageSource(),
"Did not find expected source code [{$code}]"
);
return $this;
}
/**
* Assert that the given source code is not present on the page.
*
* @param string $code
* @return $this
*/
public function assertSourceMissing($code)
{
PHPUnit::assertNotContains(
$code, $this->driver->getPageSource(),
"Found unexpected source code [{$code}]"
);
return $this;
}
/**
* Assert that the given link is visible.
*
* @param string $link
* @return $this
*/
public function assertSeeLink($link)
{
if ($this->resolver->prefix) {
$message = "Did not see expected link [{$link}] within [{$this->resolver->prefix}].";
} else {
$message = "Did not see expected link [{$link}].";
}
PHPUnit::assertTrue(
$this->seeLink($link),
$message
);
return $this;
}
/**
* Assert that the given link is not visible.
*
* @param string $link
* @return $this
*/
public function assertDontSeeLink($link)
{
if ($this->resolver->prefix) {
$message = "Saw unexpected link [{$link}] within [{$this->resolver->prefix}].";
} else {
$message = "Saw unexpected expected link [{$link}].";
}
PHPUnit::assertFalse(
$this->seeLink($link),
$message
);
return $this;
}
/**
* Determine if the given link is visible.
*
* @param string $link
* @return bool
*/
public function seeLink($link)
{
$this->ensurejQueryIsAvailable();
$selector = addslashes(trim($this->resolver->format("a:contains('{$link}')")));
$script = <<<JS
var link = jQuery.find("{$selector}");
return link.length > 0 && jQuery(link).is(':visible');
JS;
return $this->driver->executeScript($script);
}
/**
* Assert that the given input or text area contains the given value.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertInputValue($field, $value)
{
PHPUnit::assertEquals($value, $this->inputValue($field));
return $this;
}
/**
* Assert that the given input or text area does not contain the given value.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertInputValueIsNot($field, $value)
{
PHPUnit::assertNotEquals($value, $this->inputValue($field));
return $this;
}
/**
* Get the value of the given input or text area field.
*
* @param string $field
* @return string
*/
public function inputValue($field)
{
$element = $this->resolver->resolveForTyping($field);
return in_array($element->getTagName(), ['input', 'textarea'])
? $element->getAttribute('value')
: $element->getText();
}
/**
* Assert that the given checkbox field is checked.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertChecked($field, $value = null)
{
$element = $this->resolver->resolveForChecking($field, $value);
PHPUnit::assertTrue(
$element->isSelected(),
"Expected checkbox [{$field}] to be checked, but it wasn't."
);
return $this;
}
/**
* Assert that the given checkbox field is not checked.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertNotChecked($field, $value = null)
{
$element = $this->resolver->resolveForChecking($field, $value);
PHPUnit::assertFalse(
$element->isSelected(),
"Checkbox [{$field}] was unexpectedly checked."
);
return $this;
}
/**
* Assert that the given radio field is selected.
*
* @param string $field
* @param string $value
* @return $this
*/
function assertRadioSelected($field, $value)
{
$element = $this->resolver->resolveForRadioSelection($field, $value);
PHPUnit::assertTrue(
$element->isSelected(),
"Expected radio [{$field}] to be selected, but it wasn't."
);
return $this;
}
/**
* Assert that the given radio field is not selected.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertRadioNotSelected($field, $value = null)
{
$element = $this->resolver->resolveForRadioSelection($field, $value);
PHPUnit::assertFalse(
$element->isSelected(),
"Radio [{$field}] was unexpectedly selected."
);
return $this;
}
/**
* Assert that the given select field has the given value selected.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertSelected($field, $value)
{
PHPUnit::assertTrue(
$this->selected($field, $value),
"Expected value [{$value}] to be selected for [{$field}], but it wasn't."
);
return $this;
}
/**
* Assert that the given select field does not have the given value selected.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertNotSelected($field, $value)
{
PHPUnit::assertFalse(
$this->selected($field, $value),
"Unexpected value [{$value}] selected for [{$field}]."
);
return $this;
}
/**
* Assert that the given array of values are available to be selected.
*
* @param string $field
* @param array $values
* @return $this
*/
public function assertSelectHasOptions($field, array $values)
{
PHPUnit::assertCount(
count($values),
$this->resolver->resolveSelectOptions($field, $values),
"Expected options [".implode(',', $values)."] for selection field [{$field}] to be available."
);
return $this;
}
/**
* Assert that the given array of values are not available to be selected.
*
* @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}]."
);
return $this;
}
/**
* Assert that the given value is available to be selected on the given field.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertSelectHasOption($field, $value)
{
return $this->assertSelectHasOptions($field, [$value]);
}
/**
* Assert that the given value is not available to be selected on the given field.
*
* @param string $field
* @param string $value
* @return $this
*/
public function assertSelectMissingOption($field, $value)
{
return $this->assertSelectMissingOptions($field, [$value]);
}
/**
* Determine if the given value is selected for the given select field.
*
* @param string $field
* @param string $value
* @return bool
*/
public function selected($field, $value)
{
$element = $this->resolver->resolveForSelection($field);
return (string) $element->getAttribute('value') === (string) $value;
}
/**
* Assert that the element at the given selector has the given value.
*
* @param string $selector
* @param string $value
* @return $this
*/
public function assertValue($selector, $value)
{
$actual = $this->resolver->findOrFail($selector)->getAttribute('value');
PHPUnit::assertEquals($value, $actual);
return $this;
}
/**
* Assert that the element with the given selector is visible.
*
* @param string $selector
* @return $this
*/
public function assertVisible($selector)
{
$fullSelector = $this->resolver->format($selector);
PHPUnit::assertTrue(
$this->resolver->findOrFail($selector)->isDisplayed(),
"Element [{$fullSelector}] is not visible."
);
return $this;
}
/**
* Assert that the element with the given selector is not on the page.
*
* @param string $selector
* @return $this
*/
public function assertMissing($selector)
{
$fullSelector = $this->resolver->format($selector);
try {
$missing = ! $this->resolver->findOrFail($selector)->isDisplayed();
} catch (NoSuchElementException $e) {
$missing = true;
}
PHPUnit::assertTrue($missing, "Saw unexpected element [{$fullSelector}].");
return $this;
}
/**
* Assert that a JavaScript dialog with given message has been opened.
*
* @param string $message
* @return $this
*/
public function assertDialogOpened($message)
{
PHPUnit::assertEquals(
$message, $this->driver->switchTo()->alert()->getText()
);
return $this;
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace Laravel\Dusk\Concerns;
use Closure;
use Exception;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Facebook\WebDriver\Exception\TimeOutException;
use Facebook\WebDriver\Exception\NoSuchElementException;
trait WaitsForElements
{
/**
* Execute the given callback in a scoped browser once the selector is available.
*
* @param string $selector
* @param Closure $callback
* @param int $seconds
* @return $this
*/
public function whenAvailable($selector, Closure $callback, $seconds = 5)
{
return $this->waitFor($selector, $seconds)->with($selector, $callback);
}
/**
* Wait for the given selector to be visible.
*
* @param string $selector
* @param int $seconds
* @return $this
*/
public function waitFor($selector, $seconds = 5)
{
return $this->waitUsing($seconds, 100, function () use ($selector) {
return $this->resolver->findOrFail($selector)->isDisplayed();
}, "Waited {$seconds} seconds for selector [{$selector}].");
}
/**
* Wait for the given selector to be removed.
*
* @param string $selector
* @param int $seconds
* @return $this
*/
public function waitUntilMissing($selector, $seconds = 5)
{
return $this->waitUsing($seconds, 100, function () use ($selector) {
try {
$missing = ! $this->resolver->findOrFail($selector)->isDisplayed();
} catch (NoSuchElementException $e) {
$missing = true;
}
return $missing;
}, "Waited {$seconds} seconds for removal of selector [{$selector}].");
}
/**
* Wait for the given text to be visible.
*
* @param string $text
* @param int $seconds
* @return $this
*/
public function waitForText($text, $seconds = 5)
{
return $this->waitUsing($seconds, 100, function () use ($text) {
return Str::contains($this->resolver->findOrFail('')->getText(), $text);
}, "Waited {$seconds} seconds for text [{$text}].");
}
/**
* Wait for the given link to be visible.
*
* @param string $link
* @param int $seconds
* @return $this
*/
public function waitForLink($link, $seconds = 5)
{
return $this->waitUsing($seconds, 100, function () use ($link) {
return $this->seeLink($link);
});
}
/**
* Wait for the given location.
*
* @param string $path
* @param int $seconds
* @return $this
*/
public function waitForLocation($path, $seconds = 5)
{
return $this->waitUntil("window.location.pathname == '{$path}'", $seconds);
}
/**
* Wait until the given script returns true.
*
* @param string $script
* @param int $seconds
* @return $this
*/
public function waitUntil($script, $seconds = 5)
{
if (! Str::startsWith($script, 'return ')) {
$script = 'return '.$script;
}
if (! Str::endsWith($script, ';')) {
$script = $script.';';
}
return $this->waitUsing($seconds, 100, function () use ($script) {
return $this->driver->executeScript($script);
});
}
/**
* Wait for the current page to reload.
*
* @param Closure $callback
* @param int $seconds
* @return $this
*/
public function waitForReload($callback = null, $seconds = 5)
{
$token = str_random();
$this->driver->executeScript("window['{$token}'] = {};");
if ($callback) {
$callback($this);
}
return $this->waitUsing($seconds, 100, function () use ($token) {
return $this->driver->executeScript("return typeof window['{$token}'] === 'undefined';");
});
}
/**
* Wait for the given callback to be true.
*
* @param int $seconds
* @param int $interval
* @param Closure $callback
* @param string|null $message
* @return $this
* @throws TimeOutException
*/
public function waitUsing($seconds, $interval, Closure $callback, $message = null)
{
$this->pause($interval);
$started = Carbon::now();
while (true) {
try {
if ($callback()) {
break;
}
} catch (Exception $e) {
//
}
if ($started->lt(Carbon::now()->subSeconds($seconds))) {
throw new TimeOutException($message ?: "Waited {$seconds} seconds for callback.");
}
$this->pause($interval);
}
return $this;
}
}

View File

@@ -0,0 +1,230 @@
<?php
namespace Laravel\Dusk\Console;
use Dotenv\Dotenv;
use Illuminate\Console\Command;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\ProcessBuilder;
use Symfony\Component\Process\Exception\RuntimeException;
class DuskCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dusk';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the Dusk tests for the application';
/**
* Indicates if the project has its own PHPUnit configuration.
*
* @var boolean
*/
protected $hasPhpUnitConfiguration = false;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->ignoreValidationErrors();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->purgeScreenshots();
$this->purgeConsoleLogs();
$options = array_slice($_SERVER['argv'], 2);
return $this->withDuskEnvironment(function () use ($options) {
$process = (new ProcessBuilder())
->setTimeout(null)
->setPrefix($this->binary())
->setArguments($this->phpunitArguments($options))
->getProcess();
try {
$process->setTty(true);
} catch (RuntimeException $e) {
$this->output->writeln('Warning: '.$e->getMessage());
}
return $process->run(function ($type, $line) {
$this->output->write($line);
});
});
}
/**
* Get the PHP binary to execute.
*
* @return array
*/
protected function binary()
{
return [PHP_BINARY, 'vendor/phpunit/phpunit/phpunit'];
}
/**
* Get the array of arguments for running PHPUnit.
*
* @return array
*/
protected function phpunitArguments($options)
{
return array_merge(['-c', base_path('phpunit.dusk.xml')], $options);
}
/**
* Purge the failure screenshots
*
* @return void
*/
protected function purgeScreenshots()
{
$files = Finder::create()->files()
->in(base_path('tests/Browser/screenshots'))
->name('failure-*');
foreach ($files as $file) {
@unlink($file->getRealPath());
}
}
/**
* Purge the console logs.
*
* @return void
*/
protected function purgeConsoleLogs()
{
$files = Finder::create()->files()
->in(base_path('tests/Browser/console'))
->name('*.log');
foreach ($files as $file) {
@unlink($file->getRealPath());
}
}
/**
* Run the given callback with the Dusk configuration files.
*
* @param \Closure $callback
* @return mixed
*/
protected function withDuskEnvironment($callback)
{
if (file_exists(base_path($this->duskFile()))) {
if (file_get_contents(base_path('.env')) !== file_get_contents(base_path($this->duskFile()))) {
$this->backupEnvironment();
}
$this->refreshEnvironment();
}
$this->writeConfiguration();
return tap($callback(), function () {
$this->removeConfiguration();
if (file_exists(base_path($this->duskFile())) && file_exists(base_path('.env.backup'))) {
$this->restoreEnvironment();
}
});
}
/**
* Backup the current environment file.
*
* @return void
*/
protected function backupEnvironment()
{
copy(base_path('.env'), base_path('.env.backup'));
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.
*
* @return void
*/
protected function refreshEnvironment()
{
(new Dotenv(base_path()))->overload();
}
/**
* Write the Dusk PHPUnit configuration.
*
* @return void
*/
protected function writeConfiguration()
{
if (! file_exists($file = base_path('phpunit.dusk.xml'))) {
copy(realpath(__DIR__.'/../../stubs/phpunit.xml'), $file);
} else {
$this->hasPhpUnitConfiguration = true;
}
}
/**
* Remove the Dusk PHPUnit configuration.
*
* @return void
*/
protected function removeConfiguration()
{
if (! $this->hasPhpUnitConfiguration) {
unlink(base_path('phpunit.dusk.xml'));
}
}
/**
* Get the name of the Dusk file for the environment.
*
* @return string
*/
protected function duskFile()
{
if (file_exists(base_path($file = '.env.dusk.'.$this->laravel->environment()))) {
return $file;
}
return '.env.dusk';
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Laravel\Dusk\Console;
use Illuminate\Console\Command;
class InstallCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dusk:install';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Install Dusk into the application';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! is_dir(base_path('tests/Browser/Pages'))) {
mkdir(base_path('tests/Browser/Pages'), 0755, true);
}
if (! is_dir(base_path('tests/Browser/screenshots'))) {
$this->createScreenshotsDirectory();
}
if (! is_dir(base_path('tests/Browser/console'))) {
$this->createConsoleDirectory();
}
$subs = [
'ExampleTest.stub' => base_path('tests/Browser/ExampleTest.php'),
'HomePage.stub' => base_path('tests/Browser/Pages/HomePage.php'),
'DuskTestCase.stub' => base_path('tests/DuskTestCase.php'),
'Page.stub' => base_path('tests/Browser/Pages/Page.php'),
];
foreach ($subs as $stub => $file) {
if (! is_file($file)) {
copy(__DIR__.'/../../stubs/'.$stub, $file);
}
}
$this->info('Dusk scaffolding installed successfully.');
}
/**
* Create the screenshots directory.
*
* @return void
*/
protected function createScreenshotsDirectory()
{
mkdir(base_path('tests/Browser/screenshots'), 0755, true);
file_put_contents(base_path('tests/Browser/screenshots/.gitignore'), '*
!.gitignore
');
}
/**
* Create the console directory.
*
* @return void
*/
protected function createConsoleDirectory()
{
mkdir(base_path('tests/Browser/console'), 0755, true);
file_put_contents(base_path('tests/Browser/console/.gitignore'), '*
!.gitignore
');
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Laravel\Dusk\Console;
use Illuminate\Console\GeneratorCommand;
class MakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'dusk:make {name : The name of the class}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new Dusk test class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Test';
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return __DIR__.'/stubs/test.stub';
}
/**
* Get the destination class path.
*
* @param string $name
* @return string
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/tests'.str_replace('\\', '/', $name).'.php';
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Browser';
}
/**
* Get the root namespace for the class.
*
* @return string
*/
protected function rootNamespace()
{
return 'Tests';
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Laravel\Dusk\Console;
use Illuminate\Console\GeneratorCommand;
class PageCommand extends GeneratorCommand
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'dusk:page {name : The name of the class}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new Dusk page class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Page';
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub()
{
return __DIR__.'/stubs/page.stub';
}
/**
* Get the destination class path.
*
* @param string $name
* @return string
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/tests'.str_replace('\\', '/', $name).'.php';
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Browser\Pages';
}
/**
* Get the root namespace for the class.
*
* @return string
*/
protected function rootNamespace()
{
return 'Tests';
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace DummyNamespace;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Page as BasePage;
class DummyClass extends BasePage
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/';
}
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
$browser->assertPathIs($this->url());
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [
'@element' => '#selector',
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace DummyNamespace;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class DummyClass extends DuskTestCase
{
/**
* A Dusk test example.
*
* @return void
*/
public function testExample()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertSee('Laravel');
});
}
}

44
vendor/laravel/dusk/src/Dusk.php vendored Normal file
View File

@@ -0,0 +1,44 @@
<?php
namespace Laravel\Dusk;
use InvalidArgumentException;
class Dusk
{
/**
* Register the Dusk service provider.
*
* @param array $options
* @return void
*/
public static function register(array $options = [])
{
if (static::duskEnvironment($options)) {
app()->register(DuskServiceProvider::class);
}
}
/**
* Determine if Dusk may run in this environment.
*
* @param array $options
* @return bool
*/
protected static function duskEnvironment($options)
{
if (! isset($options['environments'])) {
return false;
}
if (is_string($options['environments'])) {
$options['environments'] = [$options['environments']];
}
if (! is_array($options['environments'])) {
throw new InvalidArgumentException("Dusk environments must be listed as an array.");
}
return app()->environment(...$options['environments']);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Laravel\Dusk;
use Exception;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class DuskServiceProvider extends ServiceProvider
{
/**
* Bootstrap any package services.
*
* @return void
*/
public function boot()
{
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/user/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@user',
]);
}
/**
* Register any package services.
*
* @return void
* @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\MakeCommand::class,
Console\PageCommand::class,
]);
}
}
}

View File

@@ -0,0 +1,377 @@
<?php
namespace Laravel\Dusk;
use Exception;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Support\Traits\Macroable;
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
*/
public function resolveForTyping($field)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
return $this->firstOrFail([
$field, "input[name='{$field}']", "textarea[name='{$field}']"
]);
}
/**
* Resolve the element for a given select "field".
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*/
public function resolveForSelection($field)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
return $this->firstOrFail([
$field, "select[name='{$field}']"
]);
}
/**
* Resolve all the options with the given value on the select field.
*
* @param string $field
* @param array $values
* @return array
*/
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 $value
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*/
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([
$field, "input[type=radio][name='{$field}'][value='{$value}']"
]);
}
/**
* Resolve the element for a given checkbox "field".
*
* @param string $field
* @param string $value
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*/
public function resolveForChecking($field, $value = null)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
$selector = "input[type=checkbox][name='{$field}']";
if (! is_null($value)) {
$selector .= "[value='{$value}']";
}
return $this->firstOrFail([
$field, $selector
]);
}
/**
* Resolve the element for a given file "field".
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*/
public function resolveForAttachment($field)
{
if (! is_null($element = $this->findById($field))) {
return $element;
}
return $this->firstOrFail([
$field, "input[type=file][name='{$field}']"
]);
}
/**
* Resolve the element for a given button.
*
* @param string $button
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*/
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
*/
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 array
*/
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 (starts_with($selector, '@') && $selector === $originalSelector) {
$selector = '[dusk="'.explode('@', $selector)[1].'"]';
}
return trim($this->prefix.' '.$selector);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Laravel\Dusk\Http\Controllers;
use Illuminate\Support\Facades\Auth;
class UserController
{
/**
* Retrieve the authenticated user identifier and class name.
*
* @param string|null $guard
* @return array
*/
public function user($guard = null)
{
$user = Auth::guard($guard)->user();
if (! $user) {
return [];
}
return [
'id' => $user->getAuthIdentifier(),
'className' => get_class($user),
];
}
/**
* Login using the given user ID / email.
*
* @param string $userId
* @param string $guard
* @return Response
*/
public function login($userId, $guard = null)
{
$model = $this->modelForGuard(
$guard = $guard ?: config('auth.defaults.guard')
);
if (str_contains($userId, '@')) {
$user = (new $model)->where('email', $userId)->first();
} else {
$user = (new $model)->find($userId);
}
Auth::guard($guard)->login($user);
}
/**
* Log the user out of the application.
*
* @param string $guard
* @return Response
*/
public function logout($guard = null)
{
Auth::guard($guard ?: config('auth.defaults.guard'))->logout();
}
/**
* Get the model for the given guard.
*
* @param string $guard
* @return string
*/
protected function modelForGuard($guard)
{
$provider = config("auth.guards.{$guard}.provider");
return config("auth.providers.{$provider}.model");
}
}

44
vendor/laravel/dusk/src/Page.php vendored Normal file
View File

@@ -0,0 +1,44 @@
<?php
namespace Laravel\Dusk;
abstract class Page
{
/**
* Get the URL for the page.
*
* @return string
*/
abstract public function url();
/**
* Assert that the browser is on the page.
*
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
//
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [];
}
/**
* Get the global element shortcuts for the site.
*
* @return array
*/
public static function siteElements()
{
return [];
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Laravel\Dusk;
use RuntimeException;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\ProcessBuilder;
trait SupportsChrome
{
/**
* The path to the custom Chromedriver binary.
*
* @var string|null
*/
protected static $chromeDriver;
/**
* The Chromedriver process instance.
*
* @var \Symfony\Component\Process\Process
*/
protected static $chromeProcess;
/**
* Start the Chromedriver process.
*
* @throws \RuntimeException if the driver file path doesn't exist.
*
* @return void
*/
public static function startChromeDriver()
{
static::$chromeProcess = static::buildChromeProcess();
static::$chromeProcess->start();
static::afterClass(function () {
static::stopChromeDriver();
});
}
/**
* Stop the Chromedriver process.
*
* @return void
*/
public static function stopChromeDriver()
{
if (static::$chromeProcess) {
static::$chromeProcess->stop();
}
}
/**
* Build the process to run the Chromedriver.
*
* @throws \RuntimeException if the driver file path doesn't exist.
*
* @return \Symfony\Component\Process\Process
*/
protected static function buildChromeProcess()
{
$driver = static::$chromeDriver
?: realpath(__DIR__.'/../bin/chromedriver-'.static::driverSuffix());
if (realpath($driver) === false) {
throw new RuntimeException("Invalid path to Chromedriver [{$driver}].");
}
return (new ProcessBuilder())
->setPrefix(realpath($driver))
->getProcess()
->setEnv(static::chromeEnvironment());
}
/**
* Set the path to the custom Chromedriver.
*
* @param string $path
* @return void
*/
public static function useChromedriver($path)
{
static::$chromeDriver = $path;
}
/**
* Get the Chromedriver environment variables.
*
* @return array
*/
protected static function chromeEnvironment()
{
if (PHP_OS === 'Darwin' || PHP_OS === 'WINNT') {
return [];
}
return ['DISPLAY' => ':0'];
}
/**
* Get the suffix for the Chromedriver binary.
*
* @return string
*/
protected static function driverSuffix()
{
switch (PHP_OS) {
case 'Darwin':
return 'mac';
case 'WINNT':
return 'win.exe';
default:
return 'linux';
}
}
}

245
vendor/laravel/dusk/src/TestCase.php vendored Normal file
View File

@@ -0,0 +1,245 @@
<?php
namespace Laravel\Dusk;
use Closure;
use Exception;
use Throwable;
use ReflectionFunction;
use Illuminate\Support\Collection;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Illuminate\Foundation\Testing\TestCase as FoundationTestCase;
abstract class TestCase extends FoundationTestCase
{
use SupportsChrome;
/**
* All of the active browser instances.
*
* @var array
*/
protected static $browsers = [];
/**
* The callbacks that should be run on class tear down.
*
* @var array
*/
protected static $afterClassCallbacks = [];
/**
* Register the base URL with Dusk.
*
* @return void
*/
protected function setUp()
{
parent::setUp();
Browser::$baseUrl = $this->baseUrl();
Browser::$storeScreenshotsAt = base_path('tests/Browser/screenshots');
Browser::$storeConsoleLogAt = base_path('tests/Browser/console');
Browser::$userResolver = function () {
return $this->user();
};
}
/**
* Tear down the Dusk test case class.
*
* @afterClass
* @return void
*/
public static function tearDownDuskClass()
{
static::closeAll();
foreach (static::$afterClassCallbacks as $callback) {
$callback();
}
}
/**
* Register an "after class" tear down callback.
*
* @param \Closure $callback
* @return void
*/
public static function afterClass(Closure $callback)
{
static::$afterClassCallbacks[] = $callback;
}
/**
* Create a new browser instance.
*
* @param \Closure $callback
* @return \Laravel\Dusk\Browser|void
* @throws \Exception
* @throws \Throwable
*/
public function browse(Closure $callback)
{
$browsers = $this->createBrowsersFor($callback);
try {
$callback(...$browsers->all());
} catch (Exception $e) {
$this->captureFailuresFor($browsers);
throw $e;
} catch (Throwable $e) {
$this->captureFailuresFor($browsers);
throw $e;
} finally {
$this->storeConsoleLogsFor($browsers);
static::$browsers = $this->closeAllButPrimary($browsers);
}
}
/**
* Create the browser instances needed for the given callback.
*
* @param \Closure $callback
* @return array
*/
protected function createBrowsersFor(Closure $callback)
{
if (count(static::$browsers) === 0) {
static::$browsers = collect([$this->newBrowser($this->createWebDriver())]);
}
$additional = $this->browsersNeededFor($callback) - 1;
for ($i = 0; $i < $additional; $i++) {
static::$browsers->push($this->newBrowser($this->createWebDriver()));
}
return static::$browsers;
}
/**
* Create a new Browser instance.
*
* @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver
* @return \Laravel\Dusk\Browser
*/
protected function newBrowser($driver)
{
return new Browser($driver);
}
/**
* Get the number of browsers needed for a given callback.
*
* @param \Closure $callback
* @return int
*/
protected function browsersNeededFor(Closure $callback)
{
return (new ReflectionFunction($callback))->getNumberOfParameters();
}
/**
* Capture failure screenshots for each browser.
*
* @param \Illuminate\Support\Collection $browsers
* @return void
*/
protected function captureFailuresFor($browsers)
{
$browsers->each(function ($browser, $key) {
$browser->screenshot('failure-'.$this->getName().'-'.$key);
});
}
/**
* Store the console output for the given browsers.
*
* @param \Illuminate\Support\Collection $browsers
* @return void
*/
protected function storeConsoleLogsFor($browsers)
{
$browsers->each(function ($browser, $key) {
$browser->storeConsoleLog($this->getName().'-'.$key);
});
}
/**
* Close all of the browsers except the primary (first) one.
*
* @param \Illuminate\Support\Collection $browsers
* @return \Illuminate\Support\Collection
*/
protected function closeAllButPrimary($browsers)
{
$browsers->slice(1)->each->quit();
return $browsers->take(1);
}
/**
* Close all of the active browsers.
*
* @return void
*/
public static function closeAll()
{
Collection::make(static::$browsers)->each->quit();
static::$browsers = collect();
}
/**
* Create the remote web driver instance.
*
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
*/
protected function createWebDriver()
{
return retry(5, function () {
return $this->driver();
}, 50);
}
/**
* Create the RemoteWebDriver instance.
*
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
*/
protected function driver()
{
return RemoteWebDriver::create(
'http://localhost:9515', DesiredCapabilities::chrome()
);
}
/**
* Determine the application's base URL.
*
* @var string
*/
protected function baseUrl()
{
return config('app.url');
}
/**
* Get a callback that returns the default user to authenticate.
*
* @return \Closure
* @throws \Exception
*/
protected function user()
{
throw new Exception("User resolver has not been set.");
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Tests;
use Laravel\Dusk\TestCase as BaseTestCase;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
abstract class DuskTestCase extends BaseTestCase
{
use CreatesApplication;
/**
* Prepare for Dusk test execution.
*
* @beforeClass
* @return void
*/
public static function prepare()
{
static::startChromeDriver();
}
/**
* Create the RemoteWebDriver instance.
*
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
*/
protected function driver()
{
$options = (new ChromeOptions)->addArguments([
'--disable-gpu',
'--headless'
]);
return RemoteWebDriver::create(
'http://localhost:9515', DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Tests\Browser;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class ExampleTest extends DuskTestCase
{
/**
* A basic browser test example.
*
* @return void
*/
public function testBasicExample()
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertSee('Laravel');
});
}
}

41
vendor/laravel/dusk/stubs/HomePage.stub vendored Normal file
View File

@@ -0,0 +1,41 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class HomePage extends Page
{
/**
* Get the URL for the page.
*
* @return string
*/
public function url()
{
return '/';
}
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @return void
*/
public function assert(Browser $browser)
{
//
}
/**
* Get the element shortcuts for the page.
*
* @return array
*/
public function elements()
{
return [
'@element' => '#selector',
];
}
}

20
vendor/laravel/dusk/stubs/Page.stub vendored Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Page as BasePage;
abstract class Page extends BasePage
{
/**
* Get the global element shortcuts for the site.
*
* @return array
*/
public static function siteElements()
{
return [
'@element' => '#selector',
];
}
}

21
vendor/laravel/dusk/stubs/phpunit.xml vendored Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Browser Test Suite">
<directory suffix="Test.php">./tests/Browser</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
</phpunit>