├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── CustomAssertions.php ├── Helpers.php └── PestExpectations.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `pest-expectations` will be documented in this file. 4 | 5 | ## 1.11.0 - 2025-04-13 6 | 7 | - add `toBeInRange` and `toBeCloseTo` 8 | 9 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.10.1...1.11.0 10 | 11 | ## 1.10.1 - 2025-03-12 12 | 13 | ### What's Changed 14 | 15 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/pest-expectations/pull/24 16 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/pest-expectations/pull/25 17 | * Bump aglipanci/laravel-pint-action from 2.4 to 2.5 by @dependabot in https://github.com/spatie/pest-expectations/pull/26 18 | * Add support for Laravel 12 by @AdamWills in https://github.com/spatie/pest-expectations/pull/27 19 | 20 | ### New Contributors 21 | 22 | * @AdamWills made their first contribution in https://github.com/spatie/pest-expectations/pull/27 23 | 24 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.10.0...1.10.1 25 | 26 | ## 1.10.0 - 2024-07-03 27 | 28 | ### What's Changed 29 | 30 | * feat: add timezone parameter to `toBeScheduled` by @innocenzi in https://github.com/spatie/pest-expectations/pull/23 31 | 32 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.9.0...1.10.0 33 | 34 | ## 1.9.0 - 2024-06-11 35 | 36 | ### What's Changed 37 | 38 | * feat: add `toBeArrayOf` expectation by @innocenzi in https://github.com/spatie/pest-expectations/pull/22 39 | 40 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.8.0...1.9.0 41 | 42 | ## 1.8.0 - 2024-04-30 43 | 44 | ### What's Changed 45 | 46 | * Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/spatie/pest-expectations/pull/21 47 | * extend expect() with a `toHavePagination` assertions by @Nielsvanpach in https://github.com/spatie/pest-expectations/pull/19 48 | 49 | ### New Contributors 50 | 51 | * @Nielsvanpach made their first contribution in https://github.com/spatie/pest-expectations/pull/19 52 | 53 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.7.0...1.8.0 54 | 55 | ## 1.7.0 - 2024-04-24 56 | 57 | ### What's Changed 58 | 59 | * Bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 by @dependabot in https://github.com/spatie/pest-expectations/pull/18 60 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/spatie/pest-expectations/pull/17 61 | * feat: add `toBeScheduled` expectation by @innocenzi in https://github.com/spatie/pest-expectations/pull/20 62 | 63 | ### New Contributors 64 | 65 | * @innocenzi made their first contribution in https://github.com/spatie/pest-expectations/pull/20 66 | 67 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.6.1...1.7.0 68 | 69 | ## 1.6.1 - 2024-03-11 70 | 71 | - support Laravel 11 72 | 73 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.6.0...1.6.1 74 | 75 | ## 1.6.0 - 2024-03-11 76 | 77 | ### What's Changed 78 | 79 | * Bump dependabot/fetch-metadata from 1.4.0 to 1.5.1 by @dependabot in https://github.com/spatie/pest-expectations/pull/11 80 | * Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 by @dependabot in https://github.com/spatie/pest-expectations/pull/13 81 | * Bump aglipanci/laravel-pint-action from 2.2.0 to 2.3.1 by @dependabot in https://github.com/spatie/pest-expectations/pull/16 82 | * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/spatie/pest-expectations/pull/15 83 | * Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/spatie/pest-expectations/pull/14 84 | 85 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.5.1...1.6.0 86 | 87 | ## 1.5.1 - 2023-05-17 88 | 89 | ### What's Changed 90 | 91 | - Fix Translator is not instantiable by @sdkakcy in https://github.com/spatie/pest-expectations/pull/10 92 | 93 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.5.0...1.5.1 94 | 95 | ## 1.5.0 - 2023-04-27 96 | 97 | ### What's Changed 98 | 99 | - Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/spatie/pest-expectations/pull/5 100 | - Added support for closures with translate method by @sdkakcy in https://github.com/spatie/pest-expectations/pull/8 101 | 102 | ### New Contributors 103 | 104 | - @sdkakcy made their first contribution in https://github.com/spatie/pest-expectations/pull/8 105 | 106 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.4.1...1.5.0 107 | 108 | ## 1.4.1 - 2023-04-21 109 | 110 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.4.0...1.4.1 111 | 112 | ## 1.4.0 - 2023-04-17 113 | 114 | - add `toBeModel` 115 | 116 | ## 1.3.0 - 2023-04-05 117 | 118 | - add support for Laravel 10 119 | 120 | ## 1.2.0 - 2023-04-01 121 | 122 | ### What's Changed 123 | 124 | - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/pest-expectations/pull/2 125 | - Bump aglipanci/laravel-pint-action from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/pest-expectations/pull/3 126 | - Add helpers by @freekmurze in https://github.com/spatie/pest-expectations/pull/4 127 | 128 | ### New Contributors 129 | 130 | - @freekmurze made their first contribution in https://github.com/spatie/pest-expectations/pull/4 131 | 132 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.1.0...1.2.0 133 | 134 | ## 1.1.0 - 2023-01-15 135 | 136 | - add `toBeEnum` 137 | 138 | **Full Changelog**: https://github.com/spatie/pest-expectations/compare/1.0.0...1.1.0 139 | 140 | ## 1.0.0 - 2023-01-15 141 | 142 | - stable release 143 | 144 | ## 0.0.1 - 2023-01-15 145 | 146 | - experimental release 147 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A collection of handy custom Pest customizations 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/pest-expectations.svg?style=flat-square)](https://packagist.org/packages/spatie/pest-expectations) 4 | [![Tests](https://img.shields.io/github/actions/workflow/status/spatie/pest-expectations/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/spatie/pest-expectations/actions/workflows/run-tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/pest-expectations.svg?style=flat-square)](https://packagist.org/packages/spatie/pest-expectations) 6 | 7 | This repo contains custom expectations to be used in a [Pest](https://pestphp.com) test suite. 8 | 9 | It also contains various helpers to make testing with Pest easier. Imagine, you only want to run a test on GitHub Actions. You can use the `whenGitHubActions` helper to do so. 10 | 11 | ```php 12 | it('can only run well on github actions', function () { 13 | // your test 14 | })->whenGitHubActions(); 15 | ``` 16 | 17 | ## Support us 18 | 19 | [](https://spatie.be/github-ad-click/pest-expectations) 20 | 21 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 22 | 23 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 24 | 25 | ## Installation 26 | 27 | You can install the package via composer: 28 | 29 | ```bash 30 | composer require spatie/pest-expectations 31 | ``` 32 | 33 | ## Usage 34 | 35 | Once installed, you can use the [custom expectations](#expectations) and [helpers](#helpers) provided by this package. 36 | 37 | ### Expectations 38 | 39 | #### toPassWith 40 | 41 | This expectation can be used to test if [an invokable validation rule](https://laravel.com/docs/master/validation#using-rule-objects) works correctly. 42 | 43 | In this example, the `$value` will be given to `YourValidationRule`. The expectation will pass if your rule passed for the given value. 44 | 45 | ```php 46 | expect(new YourValidationRule())->toPassWith($value); 47 | ``` 48 | 49 | You can expect the your validation not to pass for the given value, by using Pest's `not()`. 50 | 51 | ```php 52 | expect(new YourValidationRule()->not()->toPassWith($value); 53 | ``` 54 | 55 | #### toFailWith 56 | 57 | This expectation can be used to test if [an invokable validation rule](https://laravel.com/docs/master/validation#using-rule-objects) did not pass for a given value. 58 | 59 | In this example, the `$value` will be given to `YourValidationRule`. The expectation will pass if your rule did not pass for the given value. 60 | 61 | ```php 62 | expect(new YourValidationRule())->toFailWith($value); 63 | ``` 64 | 65 | Optionally, you can also pass a message as the second argument. The expectation will pass is the validation rule return the given `$message`. 66 | 67 | ```php 68 | expect(new YourValidationRule())->toFailWith($value, 'This value is not valid.'); 69 | ``` 70 | 71 | #### toBeModel 72 | 73 | Expect that a value is a model an equal to the passed model. 74 | 75 | ```php 76 | expect($model)->toBeModel($anotherModel); 77 | ``` 78 | 79 | The expectation will only pass if both models are Eloquent models of the same class, with the same key. 80 | 81 | #### toBeArrayOf 82 | 83 | Expect that a value is an array of the specified value. 84 | 85 | ```php 86 | expect(User::all())->toBeArrayOf(User::class); 87 | ``` 88 | 89 | The specified value may be a class name or a class instance, or one of the following string values: 90 | - `'string'` 91 | - `'int'` 92 | - `'float'` 93 | - `'bool'` 94 | - `'scalar'` 95 | - `'array'` 96 | - `'object'` 97 | - `'null'` 98 | 99 | ```php 100 | expect([1, 2])->toBeArrayOf('int'); 101 | expect([true, false])->toBeArrayOf('bool'); 102 | expect(['foo', 1, false])->toBeArrayOf('scalar'); 103 | ``` 104 | 105 | #### toBeScheduled 106 | 107 | Expect that a value is a scheduled job, command or invokable class. The `timezone` parameter, if passed, will also check that the specified timezone was defined. 108 | 109 | ```php 110 | expect(MyJob::class)->toBeScheduled('0 * * * *', timezone: 'Europe/Paris'); 111 | ``` 112 | 113 | Optionally, you may pass a callback that accepts an `Illuminate\Console\Scheduling\Event` or `Illuminate\Console\Scheduling\CallbackEvent` instance, so you can run any assertion needed: 114 | 115 | ```php 116 | expect(MyArtisanCommand::class)->toBeScheduled(function (Event $event) { 117 | expect($event->getExpression())->toBe('0 0 * * *'); 118 | expect($event->getSummaryForDisplay())->toBe('Foo'); 119 | }); 120 | ``` 121 | 122 | #### toHaveJsonApiPagination 123 | 124 | Expect that a response has JSON:API pagination. 125 | Have a look at our package [spatie/laravel-json-api-paginate](https://github.com/spatie/laravel-json-api-paginate) to see how to add JSON:API pagination to your Laravel app. 126 | 127 | ```php 128 | $response = $this->get('/'); 129 | 130 | expect($response)->toHaveJsonApiPagination(); 131 | ``` 132 | 133 | ### toBeInRange 134 | 135 | Expect that a value is in the specified range. The range is inclusive, meaning that the start and end values are included in the range. 136 | 137 | The first argument is the start of the range, and the second argument is the end of the range. 138 | 139 | ```php 140 | expect(5)->toBeInRange(1, 10); // passes 141 | expect(11)->toBeInRange(1, 10); // fails 142 | ``` 143 | 144 | ### toBeCloseTo 145 | 146 | Expect that a value is close to the specified value. The first argument is the expected value, and the second argument is the deviation. 147 | 148 | The deviation is the maximum difference between the expected value and the actual value. 149 | 150 | ```php 151 | expect(5)->toBeCloseTo(4.89, deviation: 0.1); // fails 152 | expect(5)->toBeCloseTo(4.90, deviation: 0.1); // passes 153 | expect(5)->toBeCloseTo(5.1, deviation: 0.1); // passes 154 | expect(5)->toBeCloseTo(5.11, deviation: 0.1); // fails 155 | ``` 156 | 157 | ### Helpers 158 | 159 | This package offers various helpers that you can tack on any test. Here's an example of the `whenGitHubActions` helper. When tacked on to a test, the test will be skipped unless you're running it on GitHub Actions. 160 | 161 | ```php 162 | it('can only run well on github actions', function () { 163 | // your test 164 | })->whenGitHubActions(); 165 | ``` 166 | 167 | To use the helpers, you should call `registerSpatiePestHelpers()` in your `Pest.php` file. 168 | 169 | These helpers are provided by this package: 170 | 171 | - `whenConfig($configKey)`: only run the test when the given Laravel config key is set 172 | - `whenEnvVar($envVarName)`: only run the test when the given environment variable is set 173 | - `whenWindows`: the test will be skipped unless running on Windows 174 | - `whenMac`: the test will be skipped unless running on macOS 175 | - `whenLinux`: the test will be skipped unless running on Linux 176 | - `whenGitHubActions()`: the test will be skipped unless running on GitHub Actions 177 | - `skipOnGitHubActions()`: the test will be skipped when running on GitHub Actions 178 | - `whenPhpVersion($version)`: the test will be skipped unless running on the given PHP version, or higher. You can pass a version like `8` or `8.1`. 179 | 180 | ### Assertions 181 | 182 | This package also provides a set of custom assertions that you can use in your tests. 183 | 184 | In your `TestCase` class, you can use the `CustomAssertions` trait and call the `registerCustomAssertions` method in the `setUp` method. 185 | 186 | ```php 187 | class TestCase extends \Illuminate\Foundation\Testing\TestCase 188 | { 189 | use CustomAssertions; 190 | 191 | protected function setUp(): void 192 | { 193 | parent::setUp(); 194 | 195 | $this->registerCustomAssertions(); 196 | } 197 | ``` 198 | 199 | #### assertHasJsonApiPagination 200 | 201 | Assert that a response has JSON:API pagination. 202 | Have a look at our package [spatie/laravel-json-api-paginate](https://github.com/spatie/laravel-json-api-paginate) to see how to add JSON:API pagination to your Laravel app. 203 | 204 | ```php 205 | $this 206 | ->get('/') 207 | ->assertHasJsonApiPagination(); 208 | ``` 209 | 210 | ## Testing 211 | 212 | ```bash 213 | composer test 214 | ``` 215 | 216 | ## Changelog 217 | 218 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 219 | 220 | ## Contributing 221 | 222 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 223 | 224 | ## Security Vulnerabilities 225 | 226 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 227 | 228 | ## Credits 229 | 230 | - [Freek Van der Herten](https://github.com/freekmurze) 231 | - [All Contributors](../../contributors) 232 | 233 | ## License 234 | 235 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 236 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/pest-expectations", 3 | "description": "A collection of handy custom Pest customisations", 4 | "keywords": [ 5 | "spatie", 6 | "pest-expectations" 7 | ], 8 | "homepage": "https://github.com/spatie/pest-expectations", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Freek Van der Herten", 13 | "email": "freek@spatie.be", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.2", 19 | "illuminate/database": "^10.7|^11.0|^12.0" 20 | }, 21 | "require-dev": { 22 | "illuminate/contracts": "^10.0|^11.0|^12.0", 23 | "laravel/pint": "^1.2", 24 | "orchestra/testbench": "^8.3|^9.0|^10.0", 25 | "pestphp/pest": "^3.0", 26 | "spatie/laravel-json-api-paginate": "^1.14", 27 | "spatie/ray": "^1.28" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Spatie\\PestExpectations\\": "src" 32 | }, 33 | "files": [ 34 | "src/PestExpectations.php", 35 | "src/Helpers.php" 36 | ] 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Spatie\\PestExpectations\\Tests\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/pest", 45 | "test-coverage": "vendor/bin/pest --coverage", 46 | "format": "vendor/bin/pint" 47 | }, 48 | "config": { 49 | "sort-packages": true, 50 | "allow-plugins": { 51 | "pestphp/pest-plugin": true, 52 | "phpstan/extension-installer": true 53 | } 54 | }, 55 | "minimum-stability": "dev", 56 | "prefer-stable": true 57 | } 58 | -------------------------------------------------------------------------------- /src/CustomAssertions.php: -------------------------------------------------------------------------------- 1 | assertHasJsonApiPagination(); 12 | } 13 | 14 | public function assertHasJsonApiPagination(): void 15 | { 16 | TestResponse::macro('assertHasJsonApiPagination', function () { 17 | $this->assertJsonStructure([ 18 | 'links' => [ 19 | '*' => [ 20 | 'url', 21 | 'label', 22 | 'active', 23 | ], 24 | ], 25 | 'current_page', 26 | 'first_page_url', 27 | 'from', 28 | 'last_page', 29 | 'last_page_url', 30 | 'next_page_url', 31 | 'path', 32 | 'per_page', 33 | 'prev_page_url', 34 | 'to', 35 | 'total', 36 | ]); 37 | 38 | return $this; 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Helpers.php: -------------------------------------------------------------------------------- 1 | markTestSkipped("{$key} is not set in the config file"); 9 | } 10 | } 11 | 12 | function skipOnGitHubActions(): void 13 | { 14 | if (getenv('GITHUB_ACTIONS') === 'true') { 15 | test()->markTestSkipped('This test is skipped on GitHub Actions'); 16 | } 17 | } 18 | 19 | function whenGitHubActions(): void 20 | { 21 | if (getenv('GITHUB_ACTIONS') !== 'true') { 22 | test()->markTestSkipped('This test is skipped on GitHub Actions'); 23 | } 24 | } 25 | 26 | function whenEnvVar(string $key): void 27 | { 28 | if (empty(env($key))) { 29 | test()->markTestSkipped("{$key} is not set in the .env file"); 30 | } 31 | } 32 | 33 | function whenMac(): void 34 | { 35 | if (PHP_OS_FAMILY !== 'Darwin') { 36 | test()->markTestSkipped('This test will only run on macOS'); 37 | } 38 | } 39 | 40 | function whenWindows(): void 41 | { 42 | if (PHP_OS_FAMILY !== 'Windows') { 43 | test()->markTestSkipped('This test will only run on Windows'); 44 | } 45 | } 46 | 47 | function whenLinux(): void 48 | { 49 | if (PHP_OS_FAMILY !== 'Linux') { 50 | test()->markTestSkipped('This test will only run on Linux'); 51 | } 52 | } 53 | 54 | function whenPhpVersion($phpVersion): void 55 | { 56 | if (version_compare(PHP_VERSION, $phpVersion, '<')) { 57 | test()->markTestSkipped("This test will only run on PHP {$phpVersion} or higher"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/PestExpectations.php: -------------------------------------------------------------------------------- 1 | extend('toPassWith', function (mixed $value) { 16 | $rule = $this->value; 17 | 18 | if (! $rule instanceof InvokableRule && ! $rule instanceof ValidationRule) { 19 | throw new Exception('Value is not a rule'); 20 | } 21 | 22 | $passed = true; 23 | 24 | $fail = function (?string $message = null) use (&$passed) { 25 | $passed = false; 26 | 27 | return new PotentiallyTranslatedString($message ?? 'attribute', app('translator')); 28 | }; 29 | 30 | if ($rule instanceof InvokableRule) { 31 | $rule('attribute', $value, $fail); 32 | } 33 | 34 | if ($rule instanceof ValidationRule) { 35 | $rule->validate('attribute', $value, $fail); 36 | } 37 | 38 | expect($passed)->toBeTrue(); 39 | 40 | return $this; 41 | }); 42 | 43 | expect()->extend('toFailWith', function (mixed $value, ?string $expectedMessage = null) { 44 | $rule = $this->value; 45 | 46 | if (! $rule instanceof InvokableRule && ! $rule instanceof ValidationRule) { 47 | throw new Exception('Value is not a rule'); 48 | } 49 | 50 | $passed = true; 51 | $actualMessage = null; 52 | 53 | $fail = function (?string $message = null) use (&$passed, &$actualMessage) { 54 | $passed = false; 55 | 56 | $translator = app('translator'); 57 | 58 | $actualMessage = (new PotentiallyTranslatedString($message ?? 'attribute', $translator)) 59 | ->translate() 60 | ->toString(); 61 | 62 | return new PotentiallyTranslatedString($message ?? 'attribute', $translator); 63 | }; 64 | 65 | if ($rule instanceof InvokableRule) { 66 | $rule('attribute', $value, $fail); 67 | } 68 | 69 | if ($rule instanceof ValidationRule) { 70 | $rule->validate('attribute', $value, $fail); 71 | } 72 | 73 | expect($passed)->toBeFalse(); 74 | 75 | if ($expectedMessage !== null) { 76 | expect($actualMessage)->toBe($expectedMessage); 77 | } 78 | 79 | return $this; 80 | }); 81 | 82 | expect()->extend('toBeModel', function ($argument) { 83 | expect($argument)->toBeInstanceOf(Model::class, 'Argument is not a model'); 84 | expect($this->value)->toBeInstanceOf(Model::class, 'Value is not a model'); 85 | 86 | expect($this->value)->toBeInstanceOf($argument::class, 'Value is not an instance of '.get_class($argument)); 87 | 88 | expect($this->value->getKey())->not()->toBeNull('Value model was not saved yet...'); 89 | expect($argument->getKey())->not()->toBeNull('Argument model was not saved yet...'); 90 | 91 | expect($this->value->getKey())->toBe($argument->getKey(), 'Value is not the same model'); 92 | }); 93 | 94 | expect()->extend('toBeScheduled', function (string|\Closure $callback, ?string $timezone = null) { 95 | expect(class_exists($this->value))->toBeTrue("Expected `{$this->value}` to be a class."); 96 | 97 | $schedule = resolve(Schedule::class); 98 | $event = collect($schedule->events())->first(function (CallbackEvent|Event $event) { 99 | if ($event instanceof CallbackEvent) { 100 | return $event->getSummaryForDisplay() === $this->value; 101 | } 102 | 103 | if ($event instanceof Event && is_a($this->value, Command::class, allow_string: true)) { 104 | /** @var Command */ 105 | $command = resolve($this->value); 106 | 107 | return str_contains($event->command, $command->getName()); 108 | } 109 | }); 110 | 111 | assertNotNull($event, sprintf('Expected `%s` to be scheduled.', $this->value)); 112 | 113 | if (is_string($callback)) { 114 | $callback = function (CallbackEvent|Event $event) use ($callback, $timezone) { 115 | assertEquals($callback, $event->expression); 116 | 117 | if (is_string($timezone)) { 118 | assertEquals($timezone, $event->timezone); 119 | } 120 | }; 121 | } 122 | 123 | $callback($event); 124 | 125 | return $this; 126 | }); 127 | 128 | expect()->extend('toHaveJsonApiPagination', function () { 129 | expect($this->value)->assertJsonStructure([ 130 | 'links' => [ 131 | '*' => [ 132 | 'url', 133 | 'label', 134 | 'active', 135 | ], 136 | ], 137 | 'current_page', 138 | 'first_page_url', 139 | 'from', 140 | 'last_page', 141 | 'last_page_url', 142 | 'next_page_url', 143 | 'path', 144 | 'per_page', 145 | 'prev_page_url', 146 | 'to', 147 | 'total', 148 | ]); 149 | }); 150 | 151 | expect()->extend('toBeArrayOf', function (string|object $class) { 152 | if (is_object($class)) { 153 | $class = get_class($class); 154 | } 155 | 156 | expect($this->value)->toBeArray(); 157 | 158 | foreach ($this->value as $value) { 159 | match ($class) { 160 | 'string' => expect($value)->toBeString(), 161 | 'int' => expect($value)->toBeInt(), 162 | 'integer' => expect($value)->toBeInt(), 163 | 'float' => expect($value)->toBeFloat(), 164 | 'bool' => expect($value)->toBeBool(), 165 | 'boolean' => expect($value)->toBeBool(), 166 | 'scalar' => expect($value)->toBeScalar(), 167 | 'array' => expect($value)->toBeArray(), 168 | 'object' => expect($value)->toBeObject(), 169 | 'null' => expect($value)->toBeNull(), 170 | default => expect($value)->toBeInstanceOf($class) 171 | }; 172 | } 173 | }); 174 | 175 | expect()->extend('toBeInRange', function (int|float $min, int|float $max) { 176 | if ($min > $max) { 177 | throw new InvalidArgumentException("Minimum value ({$min}) cannot be greater than maximum value {$max}."); 178 | } 179 | 180 | $inRange = ($min <= $this->value) && ($this->value <= $max); 181 | 182 | expect($inRange)->toBeTrue("Expected {$this->value} to be in range [{$min}, {$max}]."); 183 | }); 184 | 185 | expect()->extend('toBeCloseTo', function (float $expected, float $deviation) { 186 | $minimum = $expected - $deviation; 187 | $maximum = $expected + $deviation; 188 | 189 | expect($this->value)->toBeInRange($minimum, $maximum); 190 | 191 | return $this; 192 | }); 193 | --------------------------------------------------------------------------------