mirror of https://gitee.com/karson/fastadmin.git
feat: PHPUnit 支持
parent
aa9fa16451
commit
813fbcf11d
|
|
@ -17,3 +17,4 @@ composer.lock
|
|||
.vscode
|
||||
node_modules
|
||||
.user.ini
|
||||
.phpunit.result.cache
|
||||
|
|
@ -17,4 +17,5 @@ return [
|
|||
'app\admin\command\Min',
|
||||
'app\admin\command\Addon',
|
||||
'app\admin\command\Api',
|
||||
'app\common\command\Test',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: yunwuxin <448901948@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use PHPUnit\TextUI\Command;
|
||||
use think\console\Command as BaseCommand;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\Session;
|
||||
|
||||
class Test extends BaseCommand
|
||||
{
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('unit')->setDescription('phpunit')->ignoreValidationErrors();
|
||||
}
|
||||
|
||||
public function execute(Input $input, Output $output): ?int
|
||||
{
|
||||
Session::init();
|
||||
$argv = $_SERVER['argv'];
|
||||
array_shift($argv);
|
||||
array_shift($argv);
|
||||
array_unshift($argv, 'phpunit');
|
||||
return (new Command())->run($argv, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -14,6 +14,11 @@
|
|||
"email": "karson@fastadmin.net"
|
||||
}
|
||||
],
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.0",
|
||||
"topthink/framework": "dev-master",
|
||||
|
|
@ -40,5 +45,9 @@
|
|||
"type": "git",
|
||||
"url": "https://gitee.com/fastadminnet/framework.git"
|
||||
}
|
||||
]
|
||||
],
|
||||
"require-dev": {
|
||||
"symfony/dom-crawler": "^5.4",
|
||||
"phpunit/phpunit": "^8.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">./application</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||||
use tests\helpers\ApplicationTrait;
|
||||
use tests\helpers\AssertionsTrait;
|
||||
use tests\helpers\CrawlerTrait;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
use ApplicationTrait;
|
||||
use AssertionsTrait;
|
||||
use CrawlerTrait;
|
||||
|
||||
protected $baseUrl = 'http://localhost';
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace tests\feature;
|
||||
|
||||
use tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testBasicExample(): void
|
||||
{
|
||||
$this->visit('/')->seeStatusCode(200);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: yunwuxin <448901948@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace tests\helpers;
|
||||
|
||||
use think\Db;
|
||||
use think\Session;
|
||||
|
||||
trait ApplicationTrait
|
||||
{
|
||||
public function withSession(array $data): self
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
Session::set($key, $value);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function clearSession()
|
||||
{
|
||||
Session::clear();
|
||||
}
|
||||
|
||||
|
||||
protected function seeInDatabase($table, array $data): self
|
||||
{
|
||||
$count = Db::name($table)->where($data)->count();
|
||||
|
||||
$this->assertGreaterThan(0, $count, sprintf(
|
||||
'Unable to find row in database table [%s] that matched attributes [%s].', $table, json_encode($data)
|
||||
));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function notSeeInDatabase($table, array $data): self
|
||||
{
|
||||
$count = Db::name($table)->where($data)->count();
|
||||
|
||||
$this->assertEquals(0, $count, sprintf(
|
||||
'Found unexpected records in database table [%s] that matched attributes [%s].', $table, json_encode($data)
|
||||
));
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: yunwuxin <448901948@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace tests\helpers;
|
||||
|
||||
use think\response\View;
|
||||
use think\Session;
|
||||
use think\Url;
|
||||
|
||||
trait AssertionsTrait
|
||||
{
|
||||
public function assertResponseOk()
|
||||
{
|
||||
$actual = $this->response->getCode();
|
||||
|
||||
$this->assertTrue(200 == $actual, "Expected status code 200, got {$actual}.");
|
||||
}
|
||||
|
||||
public function assertResponseStatus($code)
|
||||
{
|
||||
$actual = $this->response->getCode();
|
||||
|
||||
$this->assertEquals($code, $actual, "Expected status code {$code}, got {$actual}.");
|
||||
}
|
||||
|
||||
public function assertViewHas($key, $value = null)
|
||||
{
|
||||
if (is_array($key)) {
|
||||
$this->assertViewHasAll($key);
|
||||
} else {
|
||||
if (!$this->response instanceof View) {
|
||||
$this->assertTrue(false, 'The response was not a view.');
|
||||
} else {
|
||||
if (is_null($value)) {
|
||||
$this->assertArrayHasKey($key, $this->response->getVars());
|
||||
} else {
|
||||
$this->assertEquals($value, $this->response->getVars($key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function assertViewHasAll(array $bindings)
|
||||
{
|
||||
foreach ($bindings as $key => $value) {
|
||||
if (is_int($key)) {
|
||||
$this->assertViewHas($value);
|
||||
} else {
|
||||
$this->assertViewHas($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function assertViewMissing($key)
|
||||
{
|
||||
if (!$this->response instanceof View) {
|
||||
$this->assertTrue(false, 'The response was not a view.');
|
||||
} else {
|
||||
$this->assertArrayNotHasKey($key, $this->response->getVars());
|
||||
}
|
||||
}
|
||||
|
||||
public function assertRedirectedTo($uri, $params = [])
|
||||
{
|
||||
$this->assertInstanceOf('think\response\Redirect', $this->response);
|
||||
|
||||
$this->assertEquals(Url::build($uri, $params), $this->response->getTargetUrl());
|
||||
}
|
||||
|
||||
public function assertSessionHas($key, $value = null)
|
||||
{
|
||||
if (is_array($key)) {
|
||||
$this->assertSessionHasAll($key);
|
||||
} else {
|
||||
if (is_null($value)) {
|
||||
$this->assertTrue(Session::has($key), "Session missing key: $key");
|
||||
} else {
|
||||
$this->assertEquals($value, Session::get($key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function assertSessionHasAll(array $bindings)
|
||||
{
|
||||
foreach ($bindings as $key => $value) {
|
||||
if (is_int($key)) {
|
||||
$this->assertSessionHas($value);
|
||||
} else {
|
||||
$this->assertSessionHas($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: yunwuxin <448901948@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace tests\helpers;
|
||||
|
||||
use think\App;
|
||||
use think\Cookie;
|
||||
use think\Error;
|
||||
use think\Exception;
|
||||
use think\helper\Arr;
|
||||
use think\helper\Str;
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
use Throwable;
|
||||
|
||||
trait CrawlerTrait
|
||||
{
|
||||
use InteractsWithPages;
|
||||
|
||||
protected $currentUri;
|
||||
|
||||
protected $serverVariables = [];
|
||||
|
||||
/** @var Response */
|
||||
protected $response;
|
||||
|
||||
|
||||
public function get($uri, array $headers = []): self
|
||||
{
|
||||
$server = $this->transformHeadersToServerVars($headers);
|
||||
|
||||
$this->call('GET', $uri, [], [], [], $server);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function post($uri, array $data = [], array $headers = []): self
|
||||
{
|
||||
$server = $this->transformHeadersToServerVars($headers);
|
||||
|
||||
$this->call('POST', $uri, $data, [], [], $server);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function put($uri, array $data = [], array $headers = []): self
|
||||
{
|
||||
$server = $this->transformHeadersToServerVars($headers);
|
||||
|
||||
$this->call('PUT', $uri, $data, [], [], $server);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function delete($uri, array $data = [], array $headers = []): self
|
||||
{
|
||||
$server = $this->transformHeadersToServerVars($headers);
|
||||
|
||||
$this->call('DELETE', $uri, $data, [], [], $server);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null): Response
|
||||
{
|
||||
$this->currentUri = $this->prepareUrlForRequest($uri);
|
||||
|
||||
$request = Request::create(
|
||||
$this->currentUri, $method, $parameters,
|
||||
$cookies, $files, array_replace($this->serverVariables, $server)
|
||||
);
|
||||
try {
|
||||
$response = App::run($request);
|
||||
} catch (Exception|Throwable $e) {
|
||||
$response = Error::getExceptionHandler()->render($e);
|
||||
}
|
||||
|
||||
return $this->response = $response;
|
||||
}
|
||||
|
||||
|
||||
public function seeJson($data = null, $negate = false)
|
||||
{
|
||||
if (is_null($data)) {
|
||||
$this->assertJson(
|
||||
$this->response->getContent(), "JSON was not returned from [{$this->currentUri}]."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->seeJsonContains($data, $negate);
|
||||
}
|
||||
|
||||
public function seeJsonEquals(array $data): self
|
||||
{
|
||||
$actual = json_encode(Arr::sortRecursive(
|
||||
json_decode($this->response->getContent(), true)
|
||||
));
|
||||
|
||||
$this->assertEquals(json_encode(Arr::sortRecursive($data)), $actual);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function seeJsonContains(array $data, $negate = false): self
|
||||
{
|
||||
$method = $negate ? 'assertFalse' : 'assertTrue';
|
||||
|
||||
$actual = json_decode($this->response->getContent(), true);
|
||||
|
||||
if (is_null($actual) || $actual === false) {
|
||||
$this->fail('Invalid JSON was returned from the route. Perhaps an exception was thrown?');
|
||||
}
|
||||
|
||||
$actual = json_encode(Arr::sortRecursive(
|
||||
(array)$actual
|
||||
));
|
||||
|
||||
foreach (Arr::sortRecursive($data) as $key => $value) {
|
||||
$expected = $this->formatToExpectedJson($key, $value);
|
||||
|
||||
$this->{$method}(
|
||||
Str::contains($actual, $expected),
|
||||
($negate ? 'Found unexpected' : 'Unable to find') . " JSON fragment [{$expected}] within [{$actual}]."
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given key and value into a JSON string for expectation checks.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
protected function formatToExpectedJson(string $key, $value): string
|
||||
{
|
||||
$expected = json_encode([$key => $value]);
|
||||
|
||||
if (Str::startsWith($expected, '{')) {
|
||||
$expected = substr($expected, 1);
|
||||
}
|
||||
|
||||
if (Str::endsWith($expected, '}')) {
|
||||
$expected = substr($expected, 0, -1);
|
||||
}
|
||||
|
||||
return $expected;
|
||||
}
|
||||
|
||||
protected function seeModule($module): self
|
||||
{
|
||||
$this->assertEquals($module, request()->module());
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function seeController($controller): self
|
||||
{
|
||||
$this->assertEquals($controller, request()->controller());
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function seeAction($action): self
|
||||
{
|
||||
$this->assertEquals($action, request()->action());
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
protected function seeStatusCode($status): self
|
||||
{
|
||||
$this->assertEquals($status, $this->response->getCode());
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function seeHeader($headerName, $value = null): self
|
||||
{
|
||||
$headers = $this->response->getHeader();
|
||||
|
||||
$this->assertTrue(!empty($headers[$headerName]), "Header [{$headerName}] not present on response.");
|
||||
|
||||
if (!is_null($value)) {
|
||||
$this->assertEquals(
|
||||
$headers[$headerName], $value,
|
||||
"Header [{$headerName}] was found, but value [{$headers[$headerName]}] does not match [{$value}]."
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function seeCookie($cookieName, $value = null): self
|
||||
{
|
||||
|
||||
$exist = Cookie::has($cookieName);
|
||||
|
||||
$this->assertTrue($exist, "Cookie [{$cookieName}] not present on response.");
|
||||
|
||||
if (!is_null($value)) {
|
||||
$cookie = Cookie::get($cookieName);
|
||||
$this->assertEquals(
|
||||
$cookie, $value,
|
||||
"Cookie [{$cookieName}] was found, but value [{$cookie}] does not match [{$value}]."
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function withServerVariables(array $server): self
|
||||
{
|
||||
$this->serverVariables = $server;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function transformHeadersToServerVars(array $headers): array
|
||||
{
|
||||
$server = [];
|
||||
$prefix = 'HTTP_';
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
$name = strtr(strtoupper($name), '-', '_');
|
||||
|
||||
if (!Str::startsWith($name, $prefix) && $name != 'CONTENT_TYPE') {
|
||||
$name = $prefix . $name;
|
||||
}
|
||||
|
||||
$server[$name] = $value;
|
||||
}
|
||||
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace tests\helpers;
|
||||
|
||||
use PHPUnit\Framework\Exception;
|
||||
|
||||
class HttpException extends Exception
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,554 @@
|
|||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: yunwuxin <448901948@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace tests\helpers;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Exception as PHPUnitException;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\DomCrawler\Form;
|
||||
use think\File;
|
||||
use think\helper\Str;
|
||||
use think\Request;
|
||||
use think\response\Redirect;
|
||||
use think\Url;
|
||||
|
||||
trait InteractsWithPages
|
||||
{
|
||||
|
||||
/** @var \Symfony\Component\DomCrawler\Crawler */
|
||||
protected $crawler;
|
||||
|
||||
/**
|
||||
* All of the stored inputs for the current page.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $inputs = [];
|
||||
|
||||
/**
|
||||
* All of the stored uploads for the current page.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $uploads = [];
|
||||
|
||||
public function visit($uri)
|
||||
{
|
||||
return $this->makeRequest('GET', $uri);
|
||||
}
|
||||
|
||||
|
||||
protected function submitForm($buttonText, $inputs = [], $uploads = [])
|
||||
{
|
||||
$this->makeRequestUsingForm($this->fillForm($buttonText, $inputs), $uploads);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
protected function see($text, $negate = false)
|
||||
{
|
||||
$method = $negate ? 'assertNotRegExp' : 'assertRegExp';
|
||||
|
||||
$rawPattern = preg_quote($text, '/');
|
||||
|
||||
$escapedPattern = preg_quote(htmlentities($text, ENT_QUOTES, 'UTF-8', false), '/');
|
||||
|
||||
$pattern = $rawPattern == $escapedPattern
|
||||
? $rawPattern : "({$rawPattern}|{$escapedPattern})";
|
||||
|
||||
$this->$method("/$pattern/i", $this->response->getContent());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function notSee($text)
|
||||
{
|
||||
return $this->see($text, true);
|
||||
}
|
||||
|
||||
public function seeInElement($element, $text, $negate = false)
|
||||
{
|
||||
if ($negate) {
|
||||
return $this->notSeeInElement($element, $text);
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$this->hasInElement($element, $text),
|
||||
"Element [$element] should contain the expected text [{$text}]"
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function notSeeInElement($element, $text)
|
||||
{
|
||||
$this->assertFalse(
|
||||
$this->hasInElement($element, $text),
|
||||
"Element [$element] should not contain the expected text [{$text}]"
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function seeLink($text, $url = null)
|
||||
{
|
||||
$message = "No links were found with expected text [{$text}]";
|
||||
|
||||
if ($url) {
|
||||
$message .= " and URL [{$url}]";
|
||||
}
|
||||
|
||||
$this->assertTrue($this->hasLink($text, $url), "{$message}.");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function notSeeLink($text, $url = null)
|
||||
{
|
||||
$message = "A link was found with expected text [{$text}]";
|
||||
|
||||
if ($url) {
|
||||
$message .= " and URL [{$url}]";
|
||||
}
|
||||
|
||||
$this->assertFalse($this->hasLink($text, $url), "{$message}.");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function hasLink($text, $url = null)
|
||||
{
|
||||
$links = $this->crawler->selectLink($text);
|
||||
|
||||
if ($links->count() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the URL is null, we assume the developer only wants to find a link
|
||||
// with the given text regardless of the URL. So, if we find the link
|
||||
// we will return true now. Otherwise, we look for the given URL.
|
||||
if ($url == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$url = $this->addRootToRelativeUrl($url);
|
||||
|
||||
/** @var \DOMElement $link */
|
||||
foreach ($links as $link) {
|
||||
if ($link->getAttribute('href') == $url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function addRootToRelativeUrl($url)
|
||||
{
|
||||
if (!Str::startsWith($url, ['http', 'https'])) {
|
||||
return Url::build($url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function seeInField($selector, $expected)
|
||||
{
|
||||
$this->assertSame(
|
||||
$expected, $this->getInputOrTextAreaValue($selector),
|
||||
"The field [{$selector}] does not contain the expected value [{$expected}]."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function notSeeInField($selector, $value)
|
||||
{
|
||||
$this->assertNotSame(
|
||||
$this->getInputOrTextAreaValue($selector), $value,
|
||||
"The input [{$selector}] should not contain the value [{$value}]."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function seeIsChecked($selector)
|
||||
{
|
||||
$this->assertTrue(
|
||||
$this->isChecked($selector),
|
||||
"The checkbox [{$selector}] is not checked."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function notSeeIsChecked($selector)
|
||||
{
|
||||
$this->assertFalse(
|
||||
$this->isChecked($selector),
|
||||
"The checkbox [{$selector}] is checked."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function isChecked($selector)
|
||||
{
|
||||
$checkbox = $this->filterByNameOrId($selector, "input[type='checkbox']");
|
||||
|
||||
if ($checkbox->count() == 0) {
|
||||
throw new Exception("There are no checkbox elements with the name or ID [$selector].");
|
||||
}
|
||||
|
||||
return $checkbox->attr('checked') !== null;
|
||||
}
|
||||
|
||||
public function seeIsSelected($selector, $expected)
|
||||
{
|
||||
$this->assertEquals(
|
||||
$expected, $this->getSelectedValue($selector),
|
||||
"The field [{$selector}] does not contain the selected value [{$expected}]."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function notSeeIsSelected($selector, $value)
|
||||
{
|
||||
$this->assertNotEquals(
|
||||
$value, $this->getSelectedValue($selector),
|
||||
"The field [{$selector}] contains the selected value [{$value}]."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getSelectedValue($selector)
|
||||
{
|
||||
$field = $this->filterByNameOrId($selector);
|
||||
|
||||
if ($field->count() == 0) {
|
||||
throw new Exception("There are no elements with the name or ID [$selector].");
|
||||
}
|
||||
|
||||
$element = $field->nodeName();
|
||||
|
||||
if ($element == 'select') {
|
||||
return $this->getSelectedValueFromSelect($field);
|
||||
}
|
||||
|
||||
if ($element == 'input') {
|
||||
return $this->getCheckedValueFromRadioGroup($field);
|
||||
}
|
||||
|
||||
throw new Exception("Given selector [$selector] is not a select or radio group.");
|
||||
}
|
||||
|
||||
protected function getSelectedValueFromSelect(Crawler $field)
|
||||
{
|
||||
if ($field->nodeName() !== 'select') {
|
||||
throw new Exception('Given element is not a select element.');
|
||||
}
|
||||
|
||||
/** @var \DOMElement $option */
|
||||
foreach ($field->children() as $option) {
|
||||
if ($option->hasAttribute('selected')) {
|
||||
return $option->getAttribute('value');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getCheckedValueFromRadioGroup(Crawler $radioGroup)
|
||||
{
|
||||
if ($radioGroup->nodeName() !== 'input' || $radioGroup->attr('type') !== 'radio') {
|
||||
throw new Exception('Given element is not a radio button.');
|
||||
}
|
||||
|
||||
/** @var \DOMElement $radio */
|
||||
foreach ($radioGroup as $radio) {
|
||||
if ($radio->hasAttribute('checked')) {
|
||||
return $radio->getAttribute('value');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function click($name)
|
||||
{
|
||||
$link = $this->crawler->selectLink($name);
|
||||
|
||||
if (!count($link)) {
|
||||
$link = $this->filterByNameOrId($name, 'a');
|
||||
|
||||
if (!count($link)) {
|
||||
throw new InvalidArgumentException(
|
||||
"Could not find a link with a body, name, or ID attribute of [{$name}]."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->visit($link->link()->getUri());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function type($text, $element)
|
||||
{
|
||||
return $this->storeInput($element, $text);
|
||||
}
|
||||
|
||||
protected function check($element)
|
||||
{
|
||||
return $this->storeInput($element, true);
|
||||
}
|
||||
|
||||
protected function uncheck($element)
|
||||
{
|
||||
return $this->storeInput($element, false);
|
||||
}
|
||||
|
||||
protected function select($option, $element)
|
||||
{
|
||||
return $this->storeInput($element, $option);
|
||||
}
|
||||
|
||||
protected function attach($absolutePath, $element)
|
||||
{
|
||||
$this->uploads[$element] = $absolutePath;
|
||||
|
||||
return $this->storeInput($element, $absolutePath);
|
||||
}
|
||||
|
||||
protected function press($buttonText)
|
||||
{
|
||||
return $this->submitForm($buttonText, $this->inputs, $this->uploads);
|
||||
}
|
||||
|
||||
protected function getInputOrTextAreaValue($selector)
|
||||
{
|
||||
$field = $this->filterByNameOrId($selector, ['input', 'textarea']);
|
||||
|
||||
if ($field->count() == 0) {
|
||||
throw new Exception("There are no elements with the name or ID [$selector].");
|
||||
}
|
||||
|
||||
$element = $field->nodeName();
|
||||
|
||||
if ($element == 'input') {
|
||||
return $field->attr('value');
|
||||
}
|
||||
|
||||
if ($element == 'textarea') {
|
||||
return $field->text();
|
||||
}
|
||||
|
||||
throw new Exception("Given selector [$selector] is not an input or textarea.");
|
||||
}
|
||||
|
||||
protected function seePageIs($uri)
|
||||
{
|
||||
$this->assertPageLoaded($uri = $this->prepareUrlForRequest($uri));
|
||||
|
||||
$this->assertEquals(
|
||||
$uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function hasInElement($element, $text)
|
||||
{
|
||||
$elements = $this->crawler->filter($element);
|
||||
|
||||
$rawPattern = preg_quote($text, '/');
|
||||
|
||||
$escapedPattern = preg_quote(htmlentities($text, ENT_QUOTES, 'UTF-8', false), '/');
|
||||
|
||||
$pattern = $rawPattern == $escapedPattern
|
||||
? $rawPattern : "({$rawPattern}|{$escapedPattern})";
|
||||
|
||||
foreach ($elements as $element) {
|
||||
$element = new Crawler($element);
|
||||
|
||||
if (preg_match("/$pattern/i", $element->html())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function storeInput($element, $text)
|
||||
{
|
||||
$this->assertFilterProducesResults($element);
|
||||
|
||||
$element = str_replace('#', '', $element);
|
||||
|
||||
$this->inputs[$element] = $text;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function assertFilterProducesResults($filter)
|
||||
{
|
||||
$crawler = $this->filterByNameOrId($filter);
|
||||
|
||||
if (!count($crawler)) {
|
||||
throw new InvalidArgumentException(
|
||||
"Nothing matched the filter [{$filter}] CSS query provided for [{$this->currentUri}]."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function fillForm($buttonText, $inputs = [])
|
||||
{
|
||||
if (!is_string($buttonText)) {
|
||||
$inputs = $buttonText;
|
||||
|
||||
$buttonText = null;
|
||||
}
|
||||
|
||||
return $this->getForm($buttonText)->setValues($inputs);
|
||||
}
|
||||
|
||||
protected function getForm($buttonText = null)
|
||||
{
|
||||
try {
|
||||
if ($buttonText) {
|
||||
return $this->crawler->selectButton($buttonText)->form();
|
||||
}
|
||||
|
||||
return $this->crawler->filter('form')->form();
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new InvalidArgumentException(
|
||||
"Could not find a form that has submit button [{$buttonText}]."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function filterByNameOrId($name, $elements = '*')
|
||||
{
|
||||
$name = str_replace('#', '', $name);
|
||||
|
||||
$id = str_replace(['[', ']'], ['\\[', '\\]'], $name);
|
||||
|
||||
$elements = is_array($elements) ? $elements : [$elements];
|
||||
|
||||
array_walk($elements, function (&$element) use ($name, $id) {
|
||||
$element = "{$element}#{$id}, {$element}[name='{$name}']";
|
||||
});
|
||||
|
||||
return $this->crawler->filter(implode(', ', $elements));
|
||||
}
|
||||
|
||||
protected function makeRequest($method, $uri, $parameters = [], $cookies = [], $files = [])
|
||||
{
|
||||
$uri = $this->prepareUrlForRequest($uri);
|
||||
|
||||
$this->call($method, $uri, $parameters, $cookies, $files);
|
||||
|
||||
$this->clearInputs()->followRedirects()->assertPageLoaded($uri);
|
||||
|
||||
$this->currentUri = Request::instance()->url(true);
|
||||
|
||||
$this->crawler = new Crawler($this->response->getContent(), $this->currentUri);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function makeRequestUsingForm(Form $form, array $uploads = [])
|
||||
{
|
||||
$files = $this->convertUploadsForTesting($form, $uploads);
|
||||
|
||||
return $this->makeRequest(
|
||||
$form->getMethod(), $form->getUri(), $this->extractParametersFromForm($form), [], $files
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
protected function assertPageLoaded($uri, $message = null)
|
||||
{
|
||||
$status = $this->response->getCode();
|
||||
|
||||
try {
|
||||
$this->assertEquals(200, $status);
|
||||
} catch (PHPUnitException $e) {
|
||||
$message = $message ?: "A request to [{$uri}] failed. Received status code [{$status}].";
|
||||
|
||||
throw new HttpException($message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function convertUploadsForTesting(Form $form, array $uploads)
|
||||
{
|
||||
$files = $form->getFiles();
|
||||
|
||||
$names = array_keys($files);
|
||||
|
||||
$files = array_map(function (array $file, $name) use ($uploads) {
|
||||
return isset($uploads[$name])
|
||||
? $this->getUploadedFileForTesting($file, $uploads, $name)
|
||||
: $file;
|
||||
}, $files, $names);
|
||||
|
||||
return array_combine($names, $files);
|
||||
}
|
||||
|
||||
protected function getUploadedFileForTesting($file, $uploads, $name)
|
||||
{
|
||||
$file['name'] = basename($uploads[$name]);
|
||||
|
||||
return new File(
|
||||
$file['tmp_name'], $file, true
|
||||
);
|
||||
}
|
||||
|
||||
protected function extractParametersFromForm(Form $form)
|
||||
{
|
||||
parse_str(http_build_query($form->getValues()), $parameters);
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
protected function clearInputs()
|
||||
{
|
||||
$this->inputs = [];
|
||||
|
||||
$this->uploads = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function followRedirects()
|
||||
{
|
||||
while ($this->response instanceof Redirect) {
|
||||
$this->makeRequest('GET', $this->response->getTargetUrl());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function prepareUrlForRequest($uri)
|
||||
{
|
||||
if (Str::startsWith($uri, '/')) {
|
||||
$uri = substr($uri, 1);
|
||||
}
|
||||
|
||||
if (!Str::startsWith($uri, 'http')) {
|
||||
$uri = $this->baseUrl . '/' . $uri;
|
||||
}
|
||||
|
||||
return trim($uri, '/');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace tests\unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue