diff --git a/src/PendingObjects/TestCall.php b/src/PendingObjects/TestCall.php index 39486565..40574ed1 100644 --- a/src/PendingObjects/TestCall.php +++ b/src/PendingObjects/TestCall.php @@ -7,14 +7,15 @@ namespace Pest\PendingObjects; use Closure; use Pest\Factories\TestCaseFactory; use Pest\Support\Backtrace; +use Pest\Support\HigherOrderCallables; use Pest\Support\NullClosure; use Pest\TestSuite; use SebastianBergmann\Exporter\Exporter; /** - * @method \Pest\Expectations\Expectation expect(mixed $value) - * * @internal + * + * @mixin HigherOrderCallables */ final class TestCall { diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php index cc03bd17..6d993717 100644 --- a/src/Plugins/Coverage.php +++ b/src/Plugins/Coverage.php @@ -81,7 +81,6 @@ final class Coverage implements AddsOutput, HandlesArguments } if ($input->getOption(self::MIN_OPTION) !== null) { - /* @phpstan-ignore-next-line */ $this->coverageMin = (float) $input->getOption(self::MIN_OPTION); } diff --git a/src/Support/HigherOrderCallables.php b/src/Support/HigherOrderCallables.php new file mode 100644 index 00000000..46503e67 --- /dev/null +++ b/src/Support/HigherOrderCallables.php @@ -0,0 +1,65 @@ +target = $target; + } + + /** + * @template TValue + * + * Create a new expectation. Callable values will be executed prior to returning the new expectation. + * + * @param callable|TValue $value + * + * @return Expectation + */ + public function expect($value) + { + return new Expectation(is_callable($value) ? Reflection::bindCallable($value) : $value); + } + + /** + * @template TValue + * + * Create a new expectation. Callable values will be executed prior to returning the new expectation. + * + * @param callable|TValue $value + * + * @return Expectation + */ + public function and($value) + { + return $this->expect($value); + } + + /** + * @template TValue + * + * @param callable(): TValue $callable + * + * @return TValue|object + */ + public function tap(callable $callable) + { + Reflection::bindCallable($callable); + + return $this->target; + } +} diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index 9f363b5a..2a51b7ba 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -70,6 +70,11 @@ final class HigherOrderMessage */ public function call(object $target) { + if ($this->hasHigherOrderCallable()) { + /* @phpstan-ignore-next-line */ + return (new HigherOrderCallables($target))->{$this->methodName}(...$this->arguments); + } + try { return Reflection::call($target, $this->methodName, $this->arguments); } catch (Throwable $throwable) { @@ -88,6 +93,16 @@ final class HigherOrderMessage } } + /** + * Determines whether or not there exists a higher order callable with the message name. + * + * @return bool + */ + private function hasHigherOrderCallable() + { + return in_array($this->methodName, get_class_methods(HigherOrderCallables::class), true); + } + private static function getUndefinedMethodMessage(object $target, string $methodName): string { if (\PHP_MAJOR_VERSION >= 8) { diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index c25709b9..fbe7ba40 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -41,15 +41,25 @@ final class Reflection } if (is_callable($method)) { - return Closure::fromCallable($method)->bindTo( - TestSuite::getInstance()->test - )(...$args); + return static::bindCallable($method, $args); } throw $exception; } } + /** + * Bind a callable to the TestCase and return the result. + * + * @param array $args + * + * @return mixed + */ + public static function bindCallable(callable $callable, array $args = []) + { + return Closure::fromCallable($callable)->bindTo(TestSuite::getInstance()->test)(...$args); + } + /** * Infers the file name from the given closure. */ diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 9b0a5c44..d943da92 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -410,6 +410,8 @@ PASS Tests\Features\HigherOrderTests ✓ it proxies calls to object ✓ it is capable doing multiple assertions + ✓ it resolves expect callables correctly + ✓ it can tap into the test WARN Tests\Features\Incompleted … incompleted @@ -579,5 +581,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 7 skipped, 363 passed + Tests: 4 incompleted, 7 skipped, 365 passed \ No newline at end of file diff --git a/tests/Features/HigherOrderTests.php b/tests/Features/HigherOrderTests.php index 77e87ce4..f871d919 100644 --- a/tests/Features/HigherOrderTests.php +++ b/tests/Features/HigherOrderTests.php @@ -1,5 +1,7 @@ assertTrue(true); it('proxies calls to object')->assertTrue(true); @@ -8,4 +10,18 @@ it('is capable doing multiple assertions') ->assertTrue(true) ->assertFalse(false); +it('resolves expect callables correctly') + ->expect(function () { return 'foo'; }) + ->toBeString() + ->toBe('foo') + ->and('bar') + ->toBeString() + ->toBe('bar'); + +it('can tap into the test') + ->expect('foo')->toBeString() + ->tap(function () { expect($this)->toBeInstanceOf(TestCase::class); }) + ->toBe('foo') + ->and('hello world')->toBeString(); + afterEach()->assertTrue(true);