diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 389f0cf9..ce089b2d 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -29,12 +29,17 @@ trait Extendable static::$extends[$name] = $extend; } + /** + * Register a a pipe to be applied before an expectation is checked. + */ public static function pipe(string $name, Closure $pipe): void { self::$pipes[$name][] = $pipe; } /** + * Recister an interceptor that should replace an existing expectation. + * * @param string|Closure $filter */ public static function intercept(string $name, $filter, Closure $handler): void @@ -46,9 +51,7 @@ trait Extendable } //@phpstan-ignore-next-line - self::pipe($name, function (...$arguments) use ($handler, $filter) { - $next = array_pop($arguments); - + self::pipe($name, function ($next, ...$arguments) use ($handler, $filter) { //@phpstan-ignore-next-line if ($filter($this->value)) { //@phpstan-ignore-next-line @@ -57,7 +60,7 @@ trait Extendable return; } - $next(...$arguments); + $next(); }); } diff --git a/src/Exceptions/PipeException.php b/src/Exceptions/PipeException.php index c41e221e..9d6c2a27 100644 --- a/src/Exceptions/PipeException.php +++ b/src/Exceptions/PipeException.php @@ -1,17 +1,14 @@ value) ? $this->value : iterator_to_array($this->value); - $keys = array_keys($value); - $values = array_values($value); + $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); + $keys = array_keys($value); + $values = array_values($value); $callbacksCount = count($callbacks); $index = 0; while (count($callbacks) < count($values)) { $callbacks[] = $callbacks[$index]; - $index = $index < count($values) - 1 ? $index + 1 : 0; + $index = $index < count($values) - 1 ? $index + 1 : 0; } if ($callbacksCount > count($values)) { @@ -170,7 +170,7 @@ final class Expectation * * @template TMatchSubject of array-key * - * @param callable(): TMatchSubject|TMatchSubject $subject + * @param callable(): TMatchSubject|TMatchSubject $subject * @param array): mixed)|TValue> $expressions */ public function match($subject, array $expressions): Expectation @@ -273,9 +273,16 @@ final class Expectation private function getExpectationClosure(string $name): Closure { if (method_exists($this->coreExpectation, $name)) { + //@phpstan-ignore-next-line return Closure::fromCallable([$this->coreExpectation, $name]); - } elseif (self::hasExtend($name)) { - return self::$extends[$name]; + } + + if (self::hasExtend($name)) { + $extend = self::$extends[$name]->bindTo($this, Expectation::class); + + if ($extend != false) { + return $extend; + } } throw PipeException::expectationNotFound($name); diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php index bef4bdc6..9ebba221 100644 --- a/src/Support/ExpectationPipeline.php +++ b/src/Support/ExpectationPipeline.php @@ -5,8 +5,6 @@ declare(strict_types=1); namespace Pest\Support; use Closure; -use Pest\Exceptions\PipeException; -use ReflectionFunction; final class ExpectationPipeline { @@ -25,7 +23,7 @@ final class ExpectationPipeline public function __construct(string $expectationName, Closure $expectationClosure) { $this->expectationClosure = $expectationClosure; - $this->expectationName = $expectationName; + $this->expectationName = $expectationName; } public static function for(string $expectationName, Closure $expectationClosure): self @@ -39,6 +37,7 @@ final class ExpectationPipeline public function send(...$passable): self { $this->passable = $passable; + return $this; } @@ -57,58 +56,20 @@ final class ExpectationPipeline $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), - $this->expectationClosure + function (): void { + ($this->expectationClosure)(...$this->passable); + } ); - $pipeline(...$this->passable); + $pipeline(); } public function carry(): Closure { return function ($stack, $pipe): Closure { - return function (...$passable) use ($stack, $pipe) { - $this->checkOptionalParametersBecomeRequired($pipe); - - $passable = $this->preparePassable($passable); - - $passable[] = $stack; - - return $pipe(...$passable); + return function () use ($stack, $pipe) { + return $pipe($stack, ...$this->passable); }; }; } - - - private function preparePassable(array $passable): array - { - $reflection = new ReflectionFunction($this->expectationClosure); - - $requiredParametersCount = $reflection->getNumberOfParameters(); - - - if (count($passable) < $requiredParametersCount) { - foreach ($reflection->getParameters() as $index => $parameter) { - if (!isset($passable[$index])) { - $passable[$index] = $parameter->getDefaultValue(); - } - } - } - - return $passable; - } - - private function checkOptionalParametersBecomeRequired($pipe) - { - $reflection = new ReflectionFunction($pipe); - - foreach ($reflection->getParameters() as $parameter) { - if ($parameter->isDefaultValueAvailable()) { - /* - * TODO add pipeline blame in the exception message and a stronger clarification like - * “You’re attempting to pipe ‘toBe’, but haven’t added the $actual parameter to your pipe handler” - */ - throw PipeException::optionalParmetersShouldBecomeRequired($this->expectationName); - } - } - } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 73cd151e..eb3c3000 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -179,6 +179,8 @@ PASS Tests\Features\Expect\pipe ✓ pipe is applied and can stop pipeline + ✓ interceptor works with negated expectation + ✓ pipe works with negated expectation ✓ pipe is run and can let the pipeline keep going ✓ intercept is applied ✓ intercept stops the pipeline @@ -728,5 +730,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 484 passed + Tests: 4 incompleted, 9 skipped, 486 passed \ No newline at end of file diff --git a/tests/Features/Expect/pipe.php b/tests/Features/Expect/pipe.php index d21ea5ed..6234a502 100644 --- a/tests/Features/Expect/pipe.php +++ b/tests/Features/Expect/pipe.php @@ -1,7 +1,6 @@ pipe('toBe', function ($expected, $next) use ($state) { +expect()->pipe('toBe', function ($next, $expected) use ($state) { $state->runCount['character']++; if ($this->value instanceof Character) { @@ -79,7 +78,7 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) { return; } - $next($expected); + $next(); }); /* @@ -105,7 +104,7 @@ expect()->intercept('toBe', function ($value) { /* * Overrides toBe to assert two Symbols are the same */ -expect()->pipe('toBe', function ($expected, $next) use ($state) { +expect()->pipe('toBe', function ($next, $expected) use ($state) { $state->runCount['symbol']++; if ($this->value instanceof Symbol) { @@ -116,16 +115,9 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) { return; } - $next($expected); + $next(); }); -expect()->intercept('toHaveProperty', function ($value) { - return $value instanceof Symbol && $value->value == '£'; -}, function (string $propertyName, $propertyValue = null) { - assertEquals("£", $this->value->value); -}); - - test('pipe is applied and can stop pipeline', function () use ($state) { $letter = new Character('A'); @@ -145,8 +137,19 @@ test('pipe is applied and can stop pipeline', function () use ($state) { 'wildcard' => 0, 'symbol' => 0, ]); -}) -; +}); + +test('interceptor works with negated expectation', function () { + $letter = new Number(1); + + expect($letter)->not->toBe(new Character('B')); +}); + +test('pipe works with negated expectation', function () { + $letter = new Character('A'); + + expect($letter)->not->toBe(new Character('B')); +}); test('pipe is run and can let the pipeline keep going', function () use ($state) { $state->reset(); @@ -216,8 +219,3 @@ test('intercept can be filtered with a closure', function () use ($state) { ->runCount->toHaveKey('wildcard', 1) ->appliedCount->toHaveKey('wildcard', 1); }); - -test('intercept can handle default values', function(){ - expect(new Symbol("£"))->toHaveProperty('value'); - expect(new Symbol("£"))->toHaveProperty('value', '£'); -})->only();