diff --git a/composer.json b/composer.json index e708b1e..a6022c2 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "symfony/css-selector": "^4.4|^5.0", "symfony/framework-bundle": "^4.4|^5.0", "symfony/polyfill-php80": "^1.20", + "zenstruck/assert": "^1.0", "zenstruck/callback": "^1.1" }, "require-dev": { diff --git a/src/Browser.php b/src/Browser.php index c5aebe6..ac96bdc 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -9,12 +9,12 @@ use Behat\Mink\Mink; use Behat\Mink\Session; use Behat\Mink\WebAssert; -use PHPUnit\Framework\Assert as PHPUnit; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Filesystem\Filesystem; +use Zenstruck\Browser\Assertion\MinkAssertion; +use Zenstruck\Browser\Assertion\SameUrlAssertion; use Zenstruck\Browser\Component; use Zenstruck\Browser\Response; -use Zenstruck\Browser\Test\Constraint\UrlMatches; use Zenstruck\Callback\Parameter; /** @@ -57,13 +57,13 @@ final public function visit(string $uri): self } /** - * @param array $parts The url parts to check (@see parse_url) + * @param array $parts The url parts to check {@see parse_url} (use empty array for "all") * * @return static */ final public function assertOn(string $expected, array $parts = ['path', 'query', 'fragment']): self { - PHPUnit::assertThat($expected, new UrlMatches($this->minkSession()->getCurrentUrl(), $parts)); + Assert::run(new SameUrlAssertion($this->minkSession()->getCurrentUrl(), $expected, $parts)); return $this; } @@ -75,10 +75,7 @@ final public function assertOn(string $expected, array $parts = ['path', 'query' */ final public function assertNotOn(string $expected, array $parts = ['path', 'query', 'fragment']): self { - PHPUnit::assertThat( - $expected, - PHPUnit::logicalNot(new UrlMatches($this->minkSession()->getCurrentUrl(), $parts)) - ); + Assert::not(new SameUrlAssertion($this->minkSession()->getCurrentUrl(), $expected, $parts)); return $this; } @@ -374,12 +371,11 @@ final public function assertSelected(string $selector, string $expected): self { try { $field = $this->webAssert()->fieldExists($selector); - PHPUnit::assertTrue(true); } catch (ExpectationException $e) { - PHPUnit::fail($e->getMessage()); + Assert::fail($e->getMessage()); } - PHPUnit::assertContains($expected, (array) $field->getValue()); + Assert::that((array) $field->getValue())->contains($expected); return $this; } @@ -391,12 +387,11 @@ final public function assertNotSelected(string $selector, string $expected): sel { try { $field = $this->webAssert()->fieldExists($selector); - PHPUnit::assertTrue(true); } catch (ExpectationException $e) { - PHPUnit::fail($e->getMessage()); + Assert::fail($e->getMessage()); } - PHPUnit::assertNotContains($expected, (array) $field->getValue()); + Assert::that((array) $field->getValue())->doesNotContain($expected); return $this; } @@ -493,12 +488,7 @@ final protected function documentElement(): DocumentElement */ final protected function wrapMinkExpectation(callable $callback): self { - try { - $callback(); - PHPUnit::assertTrue(true); - } catch (ExpectationException $e) { - PHPUnit::fail($e->getMessage()); - } + Assert::run(new MinkAssertion($callback)); return $this; } diff --git a/src/Browser/Assertion/MinkAssertion.php b/src/Browser/Assertion/MinkAssertion.php new file mode 100644 index 0000000..0f407d8 --- /dev/null +++ b/src/Browser/Assertion/MinkAssertion.php @@ -0,0 +1,31 @@ + + * + * @internal + */ +final class MinkAssertion +{ + /** @var callable */ + private $callback; + + public function __construct(callable $callback) + { + $this->callback = $callback; + } + + public function __invoke(): void + { + try { + ($this->callback)(); + } catch (ExpectationException $e) { + AssertionFailed::throw($e->getMessage()); + } + } +} diff --git a/src/Browser/Assertion/SameUrlAssertion.php b/src/Browser/Assertion/SameUrlAssertion.php new file mode 100644 index 0000000..77a35f8 --- /dev/null +++ b/src/Browser/Assertion/SameUrlAssertion.php @@ -0,0 +1,77 @@ + + * + * @internal + */ +final class SameUrlAssertion implements Negatable +{ + private string $current; + private string $expected; + private array $partsToMatch; + + public function __construct(string $current, string $expected, array $partsToMatch = []) + { + $this->current = $current; + $this->expected = $expected; + $this->partsToMatch = $partsToMatch; + } + + public function __invoke(): void + { + $parsedCurrent = $this->parseUrl($this->current); + $parsedExpected = $this->parseUrl($this->expected); + + if ($parsedCurrent === $parsedExpected) { + return; + } + + AssertionFailed::throw( + 'Expected current URL ({current}) to be "{expected}" (comparing parts: "{parts}").', + \array_merge($this->context(), [ + 'compare_actual' => $parsedCurrent, + 'compare_expected' => $parsedExpected, + ]) + ); + } + + public function notFailure(): AssertionFailed + { + return new AssertionFailed( + 'Expected current URL ({current}) to not be "{expected}" (comparing parts: "{parts}").', + $this->context() + ); + } + + private function context(): array + { + return [ + 'current' => $this->current, + 'expected' => $this->expected, + 'parts' => \implode(', ', $this->partsToMatch), + ]; + } + + private function parseUrl(string $url): array + { + $parts = \parse_url(\urldecode($url)); + + if (empty($this->partsToMatch)) { + return $parts; + } + + foreach (\array_keys($parts) as $part) { + if (!\in_array($part, $this->partsToMatch, true)) { + unset($parts[$part]); + } + } + + return $parts; + } +} diff --git a/src/Browser/BrowserKitBrowser.php b/src/Browser/BrowserKitBrowser.php index cf9aced..2921ae1 100644 --- a/src/Browser/BrowserKitBrowser.php +++ b/src/Browser/BrowserKitBrowser.php @@ -2,9 +2,9 @@ namespace Zenstruck\Browser; -use PHPUnit\Framework\Assert as PHPUnit; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\HttpKernel\Profiler\Profile; +use Zenstruck\Assert; use Zenstruck\Browser; use Zenstruck\Browser\Mink\BrowserKitDriver; @@ -171,7 +171,9 @@ final public function assertStatus(int $expected): self */ final public function assertSuccessful(): self { - PHPUnit::assertTrue($this->response()->isSuccessful(), "Expected successful status code (2xx), [{$this->response()->statusCode()}] received."); + Assert::true($this->response()->isSuccessful(), 'Expected successful status code (2xx) but got {actual}.', [ + 'actual' => $this->response()->statusCode(), + ]); return $this; } @@ -185,7 +187,9 @@ final public function assertRedirected(): self throw new \RuntimeException('Cannot assert redirected if not intercepting redirects. Call ->interceptRedirects() before making the request.'); } - PHPUnit::assertTrue($this->response()->isRedirect(), "Expected redirect status code (3xx), [{$this->response()->statusCode()}] received."); + Assert::true($this->response()->isRedirect(), 'Expected redirect status code (3xx) but got {actual}.', [ + 'actual' => $this->response()->statusCode(), + ]); return $this; } @@ -226,7 +230,7 @@ final public function assertJson(string $expectedContentType = 'json'): self */ final public function assertJsonMatches(string $expression, $expected): self { - PHPUnit::assertSame($expected, $this->response()->assertJson()->search($expression)); + Assert::that($this->response()->assertJson()->search($expression))->is($expected); return $this; } diff --git a/src/Browser/PantherBrowser.php b/src/Browser/PantherBrowser.php index dbdf769..47be657 100644 --- a/src/Browser/PantherBrowser.php +++ b/src/Browser/PantherBrowser.php @@ -2,10 +2,10 @@ namespace Zenstruck\Browser; -use PHPUnit\Framework\Assert as PHPUnit; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Panther\Client; use Symfony\Component\VarDumper\VarDumper; +use Zenstruck\Assert; use Zenstruck\Browser; use Zenstruck\Browser\Mink\PantherDriver; use Zenstruck\Browser\Response\PantherResponse; @@ -53,11 +53,11 @@ final public function setConsoleLogDir(string $dir): self final public function follow(string $link): self { if (!$element = $this->documentElement()->findLink($link)) { - PHPUnit::fail(\sprintf('Link "%s" not found.', $link)); + Assert::fail('Link "%s" not found.', [$link]); } if (!$element->isVisible()) { - PHPUnit::fail(\sprintf('Link "%s" is not visible.', $link)); + Assert::fail('Link "%s" is not visible.', [$link]); } $this->documentElement()->clickLink($link); @@ -73,7 +73,7 @@ final public function assertVisible(string $selector): self return $this->wrapMinkExpectation(function() use ($selector) { $element = $this->webAssert()->elementExists('css', $selector); - PHPUnit::assertTrue($element->isVisible()); + Assert::true($element->isVisible(), 'Expected element "%s" to be visible but it isn\'t.', [$selector]); }); } @@ -85,12 +85,12 @@ final public function assertNotVisible(string $selector): self $element = $this->documentElement()->find('css', $selector); if (!$element) { - PHPUnit::assertTrue(true); + Assert::pass(); return $this; } - PHPUnit::assertFalse($element->isVisible()); + Assert::false($element->isVisible(), 'Expected element "%s" to not be visible but it is.', [$selector]); return $this; } diff --git a/src/Browser/Response.php b/src/Browser/Response.php index 9014f54..de048d0 100644 --- a/src/Browser/Response.php +++ b/src/Browser/Response.php @@ -3,8 +3,8 @@ namespace Zenstruck\Browser; use Behat\Mink\Session; -use PHPUnit\Framework\Assert as PHPUnit; use Symfony\Component\VarDumper\VarDumper; +use Zenstruck\Assert; use Zenstruck\Browser\Response\DomResponse; use Zenstruck\Browser\Response\HtmlResponse; use Zenstruck\Browser\Response\JsonResponse; @@ -60,7 +60,7 @@ final public function body(): string final public function assertJson(): JsonResponse { if (!$this instanceof JsonResponse) { - PHPUnit::fail('Not a json response.'); + Assert::fail('Not a json response.'); } return $this; @@ -69,7 +69,7 @@ final public function assertJson(): JsonResponse final public function assertXml(): XmlResponse { if (!$this instanceof XmlResponse) { - PHPUnit::fail('Not an xml response.'); + Assert::fail('Not an xml response.'); } return $this; @@ -78,7 +78,7 @@ final public function assertXml(): XmlResponse final public function assertHtml(): HtmlResponse { if (!$this instanceof HtmlResponse) { - PHPUnit::fail('Not an html response.'); + Assert::fail('Not an html response.'); } return $this; @@ -87,7 +87,7 @@ final public function assertHtml(): HtmlResponse final public function assertDom(): DomResponse { if (!$this instanceof DomResponse) { - PHPUnit::fail('Not an DOM response.'); + Assert::fail('Not an DOM response.'); } return $this; diff --git a/src/Browser/Test/Constraint/UrlMatches.php b/src/Browser/Test/Constraint/UrlMatches.php deleted file mode 100644 index c3c0e38..0000000 --- a/src/Browser/Test/Constraint/UrlMatches.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * @internal - */ -final class UrlMatches extends Constraint -{ - private array $partsToMatch; - private array $url; - - public function __construct(string $url, array $partsToMatch = []) - { - $this->partsToMatch = $partsToMatch; - $this->url = $this->parseUrl($url); - } - - public function toString(): string - { - return 'matches '.$this->exporter()->export($this->url); - } - - protected function matches($other): bool - { - return $this->parseUrl($other) === $this->url; - } - - protected function failureDescription($other): string - { - return parent::failureDescription($this->parseUrl($other)); - } - - private function parseUrl(string $url): array - { - $parts = \parse_url(\urldecode($url)); - - if (empty($this->partsToMatch)) { - return $parts; - } - - foreach (\array_keys($parts) as $part) { - if (!\in_array($part, $this->partsToMatch, true)) { - unset($parts[$part]); - } - } - - return $parts; - } -}