Merge branch 'master' into performs_no_expectations

This commit is contained in:
Luke Downing
2022-02-10 17:26:15 +00:00
9 changed files with 111 additions and 18 deletions

View File

@ -18,7 +18,7 @@
], ],
"require": { "require": {
"php": "^8.0", "php": "^8.0",
"nunomaduro/collision": "^5.10.0|^6.0", "nunomaduro/collision": "^5.11.0|^6.0.0",
"pestphp/pest-plugin": "^1.0.0", "pestphp/pest-plugin": "^1.0.0",
"phpunit/phpunit": "10.0.x-dev" "phpunit/phpunit": "10.0.x-dev"
}, },
@ -54,7 +54,10 @@
"prefer-stable": true, "prefer-stable": true,
"config": { "config": {
"sort-packages": true, "sort-packages": true,
"preferred-install": "dist" "preferred-install": "dist",
"allow-plugins": {
"pestphp/pest-plugin": true
}
}, },
"bin": [ "bin": [
"bin/pest" "bin/pest"

View File

@ -56,13 +56,13 @@ final class Expectation
*/ */
public function and(mixed $value): Expectation public function and(mixed $value): Expectation
{ {
return new self($value); return $value instanceof static ? $value : new self($value);
} }
/** /**
* Creates a new expectation with the decoded JSON value. * Creates a new expectation with the decoded JSON value.
* *
* @return self<mixed> * @return self<array<int|string, mixed>|bool>
*/ */
public function json(): Expectation public function json(): Expectation
{ {
@ -70,7 +70,10 @@ final class Expectation
InvalidExpectationValue::expected('string'); InvalidExpectationValue::expected('string');
} }
return $this->toBeJson()->and(json_decode($this->value, true)); /** @var array<int|string, mixed>|bool $value */
$value = json_decode($this->value, true);
return $this->toBeJson()->and($value);
} }
/** /**
@ -125,8 +128,8 @@ final class Expectation
} }
if (is_callable($callback)) { if (is_callable($callback)) {
foreach ($this->value as $item) { foreach ($this->value as $key => $item) {
$callback(new self($item)); $callback(new self($item), $key);
} }
} }
@ -148,6 +151,7 @@ final class Expectation
throw new BadMethodCallException('Expectation value is not iterable.'); throw new BadMethodCallException('Expectation value is not iterable.');
} }
//@phpstan-ignore-next-line
$value = is_array($this->value) ? $this->value : iterator_to_array($this->value); $value = is_array($this->value) ? $this->value : iterator_to_array($this->value);
$keys = array_keys($value); $keys = array_keys($value);
$values = array_values($value); $values = array_values($value);

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Expectations; namespace Pest\Expectations;
use Closure;
use Pest\Concerns\Retrievable; use Pest\Concerns\Retrievable;
use Pest\Expectation; use Pest\Expectation;
@ -79,6 +80,31 @@ final class HigherOrderExpectation
return $this->expect($value); return $this->expect($value);
} }
/**
* Scope an expectation callback to the current value in
* the HigherOrderExpectation chain.
*
* @param Closure(Expectation<TValue>): void $expectation
*
* @return HigherOrderExpectation<TOriginalValue, TOriginalValue>
*/
public function scoped(Closure $expectation): self
{
$expectation->__invoke($this->expectation);
return new self($this->original, $this->original->value);
}
/**
* Creates a new expectation with the decoded JSON value.
*
* @return self<TOriginalValue, array<string|int, mixed>|bool>
*/
public function json(): self
{
return new self($this->original, $this->expectation->json()->value);
}
/** /**
* Dynamically calls methods on the class with the given arguments. * Dynamically calls methods on the class with the given arguments.
* *

View File

@ -26,6 +26,8 @@ final class Backtrace
$current = null; $current = null;
foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) { foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) {
assert(array_key_exists(self::FILE, $trace));
if (Str::endsWith($trace[self::FILE], 'overrides/Runner/TestSuiteLoader.php')) { if (Str::endsWith($trace[self::FILE], 'overrides/Runner/TestSuiteLoader.php')) {
break; break;
} }
@ -45,7 +47,11 @@ final class Backtrace
*/ */
public static function file(): string public static function file(): string
{ {
return debug_backtrace(self::BACKTRACE_OPTIONS)[1][self::FILE]; $trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1];
assert(array_key_exists(self::FILE, $trace));
return $trace[self::FILE];
} }
/** /**
@ -53,7 +59,11 @@ final class Backtrace
*/ */
public static function dirname(): string public static function dirname(): string
{ {
return dirname(debug_backtrace(self::BACKTRACE_OPTIONS)[1][self::FILE]); $trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1];
assert(array_key_exists(self::FILE, $trace));
return dirname($trace[self::FILE]);
} }
/** /**
@ -61,6 +71,10 @@ final class Backtrace
*/ */
public static function line(): int public static function line(): int
{ {
return debug_backtrace(self::BACKTRACE_OPTIONS)[1]['line']; $trace = debug_backtrace(self::BACKTRACE_OPTIONS)[1];
assert(array_key_exists('line', $trace));
return $trace['line'];
} }
} }

View File

@ -44,7 +44,7 @@ final class HigherOrderCallables
* *
* @param callable|TValue $value * @param callable|TValue $value
* *
* @return Expectation<TValue> * @return Expectation<(callable(): mixed)|TValue>
*/ */
public function and(mixed $value) public function and(mixed $value)
{ {
@ -52,9 +52,9 @@ final class HigherOrderCallables
} }
/** /**
* Tap into the test case to perform an action and return the test case. * Execute the given callable after the test has executed the setup method.
*/ */
public function tap(callable $callable): object public function defer(callable $callable): object
{ {
Reflection::bindCallableWithData($callable); Reflection::bindCallableWithData($callable);

View File

@ -74,8 +74,37 @@ it('works with higher order tests')
->name()->toEqual('Has Methods') ->name()->toEqual('Has Methods')
->books()->each->toBeArray; ->books()->each->toBeArray;
it('can use the scoped method to lock into the given level for expectations', function () {
expect(new HasMethods())
->attributes()->scoped(fn ($attributes) => $attributes
->name->toBe('Has Methods')
->quantity->toBe(20)
)
->name()->toBeString()->toBe('Has Methods')
->newInstance()->newInstance()->scoped(fn ($instance) => $instance
->name()->toBe('Has Methods')
->quantity()->toBe(20)
->attributes()->scoped(fn ($attributes) => $attributes
->name->toBe('Has Methods')
->quantity->toBe(20)
)
);
});
it('works consistently with the json expectation method', function () {
expect(new HasMethods())
->jsonString()->json()->id->toBe(1)
->jsonString()->json()->name->toBe('Has Methods')->toBeString()
->jsonString()->json()->quantity->toBe(20)->toBeInt();
});
class HasMethods class HasMethods
{ {
public function jsonString(): string
{
return '{ "id": 1, "name": "Has Methods", "quantity": 20 }';
}
public function name() public function name()
{ {
return 'Has Methods'; return 'Has Methods';

View File

@ -48,6 +48,15 @@ it('can start a new higher order expectation using the and syntax in higher orde
->toBeArray() ->toBeArray()
->foo->toEqual('bar'); ->foo->toEqual('bar');
it('can start a new higher order expectation using the and syntax without nesting expectations', function () {
expect(new HasMethodsAndProperties())
->toBeInstanceOf(HasMethodsAndProperties::class)
->meta
->sequence(
function ($value, $key) { $value->toBeArray()->and($key)->toBe('foo'); },
);
});
class HasMethodsAndProperties class HasMethodsAndProperties
{ {
public $name = 'Has Methods and Properties'; public $name = 'Has Methods and Properties';

View File

@ -87,3 +87,11 @@ it('accepts callables', function () {
expect(static::getCount())->toBe(12); expect(static::getCount())->toBe(12);
}); });
it('passes the key of the current item to callables', function () {
expect([1, 2, 3])->each(function ($number, $key) {
expect($key)->toBeInt();
});
expect(static::getCount())->toBe(3);
});

View File

@ -21,9 +21,9 @@ it('resolves expect callables correctly')
test('does not treat method names as callables') test('does not treat method names as callables')
->expect('it')->toBeString(); ->expect('it')->toBeString();
it('can tap into the test') it('can defer a method until after test setup')
->expect('foo')->toBeString() ->expect('foo')->toBeString()
->tap(function () { expect($this)->toBeInstanceOf(TestCase::class); }) ->defer(function () { expect($this)->toBeInstanceOf(TestCase::class); })
->toBe('foo') ->toBe('foo')
->and('hello world')->toBeString(); ->and('hello world')->toBeString();
@ -32,15 +32,15 @@ it('can pass datasets into the expect callables')
->expect(function (...$numbers) { return $numbers; })->toBe([1, 2, 3]) ->expect(function (...$numbers) { return $numbers; })->toBe([1, 2, 3])
->and(function (...$numbers) { return $numbers; })->toBe([1, 2, 3]); ->and(function (...$numbers) { return $numbers; })->toBe([1, 2, 3]);
it('can pass datasets into the tap callable') it('can pass datasets into the defer callable')
->with([[1, 2, 3]]) ->with([[1, 2, 3]])
->tap(function (...$numbers) { expect($numbers)->toBe([1, 2, 3]); }); ->defer(function (...$numbers) { expect($numbers)->toBe([1, 2, 3]); });
it('can pass shared datasets into callables') it('can pass shared datasets into callables')
->with('numbers.closure.wrapped') ->with('numbers.closure.wrapped')
->expect(function ($value) { return $value; }) ->expect(function ($value) { return $value; })
->and(function ($value) { return $value; }) ->and(function ($value) { return $value; })
->tap(function ($value) { expect($value)->toBeInt(); }) ->defer(function ($value) { expect($value)->toBeInt(); })
->toBeInt(); ->toBeInt();
afterEach()->assertTrue(true); afterEach()->assertTrue(true);