From 19f193f2b642f3fe2066b0041d18ad88bfcfe269 Mon Sep 17 00:00:00 2001 From: DarkGhosthunter Date: Sun, 19 Dec 2021 01:10:00 -0300 Subject: [PATCH 1/2] Adds assertion helpers. --- README.md | 70 ++++ src/Alert.php | 13 +- src/Bag.php | 8 +- src/Facades/Alert.php | 24 +- src/LaralertsServiceProvider.php | 2 +- src/Testing/Builder.php | 379 +++++++++++++++++++ src/Testing/Fakes/BagFake.php | 111 ++++++ tests/AlertTest.php | 9 +- tests/Renderers/BootstrapRendererTest.php | 15 +- tests/Testing/BuilderTest.php | 430 ++++++++++++++++++++++ tests/Testing/Fakes/FakeTest.php | 176 +++++++++ 11 files changed, 1213 insertions(+), 24 deletions(-) create mode 100644 src/Testing/Builder.php create mode 100644 src/Testing/Fakes/BagFake.php create mode 100644 tests/Testing/BuilderTest.php create mode 100644 tests/Testing/Fakes/FakeTest.php diff --git a/README.md b/README.md index cc3e086..7be617b 100644 --- a/README.md +++ b/README.md @@ -534,6 +534,76 @@ When you receive a JSON Response, you will see the alerts appended to whichever > If your key is already present in the JSON response, Laralerts **won't overwrite the key value**. Ensure the key is never present in the response. +## Testing + +To test any alerts after a response, you can use `Alert::fake()`, which returns an Alert Bag that you can use to assert alerts. + +The fake bag will hold a copy of all the alerts generated, which allows for some convenient assertion methods. + +```php +use \DarkGhostHunter\Laralerts\Facades\Alert; + +public function test_alert_sent() +{ + $alerts = Alert::fake(); + + $this->get('something')->assertOk(); + + $alerts->assertHas(2); +} +``` + +The following assertions are available: + +| Method | Description | +|---------------------------|------------------------------------------------------------------------| +| `assertEmpty()` | Check if the alert bag doesn't contains alerts. | +| `assertNotEmpty()` | Check if the alert bag contains any alert. | +| `assertHasOne()` | Check if the alert bag contains only one alert. | +| `assertHas($count)` | Check if the alert bag contains the exact amount of alerts. | +| `assertHasPersistent()` | Check if the alert bag contains at least one persistent alert. | +| `assertHasNoPersistent()` | Check if the alert bag doesn't contains a persistent alert. | +| `assertPersistentCount()` | Check if the alert bag contains the exact amount of persistent alerts. | + +### Asserting alerts + +You can use `assertAlert()` to build expectations for the existence (or nonexistence) of specific alerts. + +```php +$bag->assertAlert()->withMessage('Hello world!')->exists(); + +$bag->assertAlert()->dismissible()->missing(); +``` + +Alternatively, you can use `count()` if you expect a specific number of alerts to match the given conditions, or `unique()` for matching only one alert. + +```php +$bag->assertAlert()->persistent()->count(3); + +$bag->assertAlert()->withTag('toast')->unique(); +``` + +The following expectations are available: + +| Method | Description | +|---------------------|---------------------------------------------------| +| `withRaw()` | Find alerts with the given raw message. | +| `withMessage()` | Find alerts with the given message. | +| `withTrans()` | Find alerts with the translated message. | +| `withTransChoice()` | Find alerts with the translated (choice) message. | +| `withAway()` | Find alerts with a link away. | +| `withTo()` | Find alerts with a link to a path. | +| `withRoute()` | Find alerts with a link to a route. | +| `withAction()` | Find alerts with a link to a controller action. | +| `withTypes()` | Find alerts with exactly the given types. | +| `persisted()` | Find alerts persisted. | +| `notPersisted()` | Find alerts not persisted. | +| `persistedAs()` | Find alerts persisted with the issued keys. | +| `dismissible()` | Find alerts dismissible. | +| `notDismissible()` | Find alerts not dismissible. | +| `withTag()` | Find alerts with all the given tags. | +| `withAnyTag()` | Find alerts with any of the given tags. | + ## Security If you discover any security related issues, please email darkghosthunter@gmail.com instead of using the issue tracker. diff --git a/src/Alert.php b/src/Alert.php index 10bef20..86481ee 100644 --- a/src/Alert.php +++ b/src/Alert.php @@ -8,11 +8,12 @@ use Illuminate\Support\Traits\Macroable; use JsonSerializable; use Stringable; - use function action; use function is_array; use function json_encode; use function route; +use function sort; +use function strcmp; use function trans; use function trim; use function url; @@ -133,7 +134,7 @@ public function getTags(): array /** * Check if the alert contains any of the given tags. - * + * * @param string ...$tags * @return bool * @internal @@ -214,6 +215,8 @@ public function types(string ...$types): static { $this->types = $types; + sort($this->types); + return $this; } @@ -275,6 +278,10 @@ public function away(string $replace, string $url, bool $blank = true): static 'blank' => $blank, ]; + usort($this->links, static function (object $first, object $second): int { + return strcmp($first->replace . $first->url, $second->replace . $second->url); + }); + return $this; } @@ -329,6 +336,8 @@ public function tag(string ...$tags): static { $this->tags = $tags; + sort($this->tags); + return $this; } diff --git a/src/Bag.php b/src/Bag.php index cb5763c..340da59 100644 --- a/src/Bag.php +++ b/src/Bag.php @@ -3,16 +3,12 @@ namespace DarkGhostHunter\Laralerts; use Closure; -use Illuminate\Contracts\Config\Repository; -use Illuminate\Contracts\Session\Session; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Traits\Macroable; - use function array_key_last; use function json_decode; use function value; - use const JSON_THROW_ON_ERROR; /** @@ -34,8 +30,8 @@ class Bag /** * Create a new Bag instance. * - * @param \Illuminate\Contracts\Session\Session $session - * @param \Illuminate\Contracts\Config\Repository $config + * @param array $tags + * @param array $persisted */ public function __construct(protected array $tags, protected array $persisted = []) { diff --git a/src/Facades/Alert.php b/src/Facades/Alert.php index a4dd38d..716f532 100644 --- a/src/Facades/Alert.php +++ b/src/Facades/Alert.php @@ -2,7 +2,11 @@ namespace DarkGhostHunter\Laralerts\Facades; +use Closure; use DarkGhostHunter\Laralerts\Bag; +use DarkGhostHunter\Laralerts\Testing\Fakes\BagFake; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Facade; /** @@ -15,8 +19,8 @@ * @method static \DarkGhostHunter\Laralerts\Alert raw(string $message) * @method static \DarkGhostHunter\Laralerts\Alert types(string ...$types) * @method static \DarkGhostHunter\Laralerts\Alert dismiss(bool $dismissible = true) - * @method static \DarkGhostHunter\Laralerts\Alert when(\Closure|bool $condition) - * @method static \DarkGhostHunter\Laralerts\Alert unless(\Closure|bool $condition) + * @method static \DarkGhostHunter\Laralerts\Alert when(Closure|bool $condition) + * @method static \DarkGhostHunter\Laralerts\Alert unless(Closure|bool $condition) * @method static \DarkGhostHunter\Laralerts\Alert away(string $replace, string $url, bool $blank = true) * @method static \DarkGhostHunter\Laralerts\Alert to(string $replace, string $url, bool $blank = false) * @method static \DarkGhostHunter\Laralerts\Alert route(string $replace, string $name, array $parameters = [], bool $blank = false) @@ -35,4 +39,20 @@ protected static function getFacadeAccessor(): string { return Bag::class; } + + /** + * Creates a fake Alert Bag. + * + * @return \DarkGhostHunter\Laralerts\Testing\Fakes\BagFake + */ + public static function fake(): BagFake + { + $fake = static::$app->make(BagFake::class, [ + 'tags' => Arr::wrap(Config::get('laralerts.tags')) + ]); + + static::swap($fake); + + return $fake; + } } diff --git a/src/LaralertsServiceProvider.php b/src/LaralertsServiceProvider.php index 492aac8..da80f3f 100644 --- a/src/LaralertsServiceProvider.php +++ b/src/LaralertsServiceProvider.php @@ -2,8 +2,8 @@ namespace DarkGhostHunter\Laralerts; -use Illuminate\Contracts\Http\Kernel; use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\Http\Kernel; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; use Illuminate\View\Compilers\BladeCompiler; diff --git a/src/Testing/Builder.php b/src/Testing/Builder.php new file mode 100644 index 0000000..af73b9a --- /dev/null +++ b/src/Testing/Builder.php @@ -0,0 +1,379 @@ +message = $message; + + return $this; + } + + /** + * Expect an alert with the message. + * + * @param string $message + * @return $this + */ + public function withMessage(string $message): static + { + return $this->withRaw(e($message)); + } + + /** + * Expect an alert with a translated message. + * + * @param string $key + * @param array $replace + * @param string|null $locale + * @return $this + */ + public function withTrans(string $key, array $replace = [], string $locale = null): static + { + return $this->withRaw(trans($key, $replace, $locale)); + } + + /** + * Expect an alert with a translated (choice) message. + * + * @param string $key + * @param Countable|int|array $number + * @param array $replace + * @param string|null $locale + * @return $this + */ + public function withTransChoice( + string $key, + Countable|int|array $number, + array $replace = [], + string $locale = null + ): static + { + return $this->withRaw(trans_choice($key, $number, $replace, $locale)); + } + + /** + * Expect an alert with a link away. + * + * @param string $replace + * @param string $url + * @param bool $blank + * @return $this + */ + public function withAway(string $replace, string $url, bool $blank = true): static + { + $this->links[] = (object) [ + 'replace' => $replace, + 'url' => $url, + 'blank' => $blank, + ]; + + usort($this->links, static function (object $first, object $second): int { + return strcmp($first->replace . $first->url, $second->replace . $second->url); + }); + + return $this; + } + + /** + * Expect an alert with a link to a path. + * + * @param string $replace + * @param string $url + * @param bool $blank + * @return $this + */ + public function withTo(string $replace, string $url, bool $blank = false): static + { + return $this->withAway($replace, url($url), $blank); + } + + /** + * Expect an alert with a link to a route. + * + * @param string $replace + * @param string $name + * @param array $parameters + * @param bool $blank + * @return $this + */ + public function withRoute(string $replace, string $name, array $parameters = [], bool $blank = false): static + { + return $this->withAway($replace, route($name, $parameters), $blank); + } + + /** + * Expect an alert with a link to an action. + * + * @param string $replace + * @param string|array $action + * @param array $parameters + * @param bool $blank + * @return $this + */ + public function withAction(string $replace, string|array $action, array $parameters = [], bool $blank = false): static + { + return $this->withAway($replace, action($action), $blank); + } + + /** + * Expect an alert with the issued types. + * + * @param string ...$types + * @return $this + */ + public function withTypes(string ...$types): static + { + $this->types = $types; + + sort($this->types); + + return $this; + } + + /** + * Expect an alert persisted. + * + * @return $this + */ + public function persisted(): static + { + $this->persisted = true; + + return $this; + } + + /** + * Expect an alert not persisted. + * + * @return $this + */ + public function notPersisted(): static + { + $this->persisted = false; + + return $this; + } + + /** + * Expect an alert persisted with the issued key. + * + * @param string|array $key + * @return $this + */ + public function persistedAs(string|array $key): static + { + $this->persisted = $key; + + return $this; + } + + /** + * Expect an alert dismissible. + * + * @return $this + */ + public function dismissible(): static + { + $this->dismiss = true; + + return $this; + } + + /** + * Expect an alert not dismissible. + * + * @return $this + */ + public function notDismissible(): static + { + $this->dismiss = false; + + return $this; + } + + /** + * Expect an alert with the given tags. + * + * @param string ...$tags + * @return $this + */ + public function withTag(string ...$tags): static + { + $this->tags = $tags; + + return $this; + } + /** + * Expect an alert with any of the given tags. + * + * @param string ...$tags + * @return $this + */ + public function withAnyTag(string ...$tags): static + { + $this->anyTag = true; + + return $this->withTag(...$tags); + } + + /** + * Returns a collection of all matching alerts. + * + * @return \Illuminate\Support\Collection + */ + protected function matches(): Collection + { + return $this->bag->added->filter(function (Alert $alert): bool { + return $this->is($alert); + }); + } + + /** + * Check if the given alert matches the expectations. + * + * @param \DarkGhostHunter\Laralerts\Alert $alert + * @return bool + */ + protected function is(Alert $alert): bool + { + if ($this->message !== null && $this->message !== $alert->getMessage()) { + return false; + } + + if ($this->dismiss !== null && $this->dismiss !== $alert->isDismissible()) { + return false; + } + + if ($this->types !== null && $this->types !== $alert->getTypes()) { + return false; + } + + if ($this->tags !== null) { + if ($this->anyTag) { + return $alert->hasAnyTag(...$this->tags); + } + + return $this->tags === $alert->getTags(); + } + + if ($this->persisted !== null) { + if (is_string($this->persisted)) { + return $this->persisted === $alert->getPersistKey(); + } + + if (is_array($this->persisted)) { + return in_array($alert->getPersistKey(), $this->persisted, true); + } + + return $this->persisted === (bool) $alert->getPersistKey(); + } + + if ($this->links !== null && $this->links != $alert->getLinks()) { + return false; + } + + return true; + } + + /** + * Assert that at least one Alert exists with the given expectations. + * + * @param string|null $message + * @return void + */ + public function exists(string $message = null): void + { + PHPUnit::assertNotEmpty($this->matches(), $message ?? "Failed to assert that at least one alert matches the expectations."); + } + + /** + * Assert that no Alert exists with the given expectations. + * + * @param string|null $message + * @return void + */ + public function missing(string $message = null): void + { + PHPUnit::assertEmpty($this->matches(), $message ?? "Failed to assert that no alert matches the expectations."); + } + + /** + * Assert that only one Alert exists with the given expectations. + * + * @param string|null $message + * @return void + */ + public function unique(string $message = null): void + { + $this->count(1, $message); + } + + /** + * Assert that the given number of Alerts matches exactly the given expectations. + * + * @param int $count + * @param string|null $message + * @return void + */ + public function count(int $count, string $message = null): void + { + $matches = $this->matches(); + + PHPUnit::assertCount( + $count, $matches, + $message ?? "Failed to assert that [{$matches->count()}] alerts match the expected [$count] count." + ); + } +} diff --git a/src/Testing/Fakes/BagFake.php b/src/Testing/Fakes/BagFake.php new file mode 100644 index 0000000..8bbc6cf --- /dev/null +++ b/src/Testing/Fakes/BagFake.php @@ -0,0 +1,111 @@ +added = $this->alerts; + } + + /** + * Finds an alert by a given key. + * + * @return \DarkGhostHunter\Laralerts\Testing\Builder + */ + public function assertAlert(): Builder + { + return new Builder($this); + } + + /** + * Assert that the alert bag has no alerts. + * + * @return void + */ + public function assertEmpty(): void + { + $this->assertAlert()->missing("Failed to assert that there is no alerts."); + } + + /** + * Assert that the alert bag has any alert. + * + * @return void + */ + public function assertNotEmpty(): void + { + $this->assertAlert()->exists("Failed to assert that there is any alert."); + } + + /** + * Assert the alert bag contains exactly one alert. + * + * @return void + */ + public function assertHasOne(): void + { + $this->assertAlert()->count(1, "Failed to assert that there is only one alert"); + } + + /** + * Assert the alert bag contains exactly the given number of alerts. + * + * @param int $count + * @return void + */ + public function assertHas(int $count): void + { + $this->assertAlert()->count($count); + } + + /** + * Assert the alert bag contains an alert persisted by the given key. + * + * @param string $key + * @return void + */ + public function assertPersisted(string $key): void + { + $this->assertAlert()->persistedAs($key)->unique( + "Failed to assert that there is persisted alerts." + ); + } + + /** + * Assert the alert bag contains persistent alerts. + * + * @return void + */ + public function assertHasPersistent(): void + { + $this->assertAlert()->persisted()->exists("Failed to assert that there is any persistent alert"); + } + + /** + * Assert the alert bag doesn't contain persistent alerts. + * + * @return void + */ + public function assertHasNoPersistent(): void + { + $this->assertAlert()->persisted()->missing("Failed to assert that there is no persistent alert."); + } +} diff --git a/tests/AlertTest.php b/tests/AlertTest.php index 0e1031b..a3955a7 100644 --- a/tests/AlertTest.php +++ b/tests/AlertTest.php @@ -7,7 +7,6 @@ use Illuminate\Support\Facades\Lang; use Illuminate\Support\Facades\URL; use Orchestra\Testbench\TestCase; - use function alert; use function app; @@ -48,7 +47,7 @@ public function test_alert_set_types(): void $alert->types('foo', 'bar', 'quz'); - static::assertEquals(['foo', 'bar', 'quz'], $alert->getTypes()); + static::assertEquals(['bar', 'foo', 'quz'], $alert->getTypes()); } public function test_alert_set_raw_message(): void @@ -163,7 +162,7 @@ public function test_alert_to_array(): void static::assertEquals( [ 'message' => 'foo', - 'types' => ['foo', 'bar'], + 'types' => ['bar', 'foo'], 'dismissible' => true, ], $alert->toArray() @@ -181,7 +180,7 @@ public function test_array_to_json(): void static::assertJson(json_encode($alert)); static::assertEquals( - '{"message":"foo","types":["foo","bar"],"dismissible":true}', + '{"message":"foo","types":["bar","foo"],"dismissible":true}', $alert->toJson() ); } @@ -226,7 +225,7 @@ public function test_tags(): void $alert->tag('foo', 'bar'); - static::assertSame(['foo', 'bar'], $alert->getTags()); + static::assertSame(['bar', 'foo'], $alert->getTags()); } public function test_to_string(): void diff --git a/tests/Renderers/BootstrapRendererTest.php b/tests/Renderers/BootstrapRendererTest.php index dc49c28..a154769 100644 --- a/tests/Renderers/BootstrapRendererTest.php +++ b/tests/Renderers/BootstrapRendererTest.php @@ -6,7 +6,6 @@ use Illuminate\Foundation\Testing\Concerns\InteractsWithViews; use Orchestra\Testbench\TestCase; use Tests\RegistersPackage; - use function alert; class BootstrapRendererTest extends TestCase @@ -55,7 +54,7 @@ public function test_renders_bootstrap_alert(): void static::assertSame( <<<'EOT'
- @@ -72,7 +71,7 @@ public function test_renders_dismissible_alert(): void static::assertEquals( <<<'EOT'
-