-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feature] add
Json::assertHas()
/assertMissing()
/assertThat()
/`a…
…ssertThatEach()` (#92)
- Loading branch information
Showing
4 changed files
with
259 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,10 +3,12 @@ | |
namespace Zenstruck\Browser; | ||
|
||
use Zenstruck\Assert; | ||
use Zenstruck\Assert\Expectation; | ||
use function JmesPath\search; | ||
|
||
/** | ||
* @author Kevin Bond <[email protected]> | ||
* @mixin Expectation | ||
*/ | ||
final class Json | ||
{ | ||
|
@@ -17,6 +19,20 @@ public function __construct(string $source) | |
$this->source = $source; | ||
} | ||
|
||
/** | ||
* @param array<mixed> $arguments | ||
*/ | ||
public function __call(string $methodName, array $arguments): self | ||
{ | ||
if (!\method_exists(Expectation::class, $methodName)) { | ||
throw new \BadMethodCallException("{$methodName} does not exist"); | ||
} | ||
|
||
Assert::that($this->decoded())->{$methodName}(...$arguments); | ||
|
||
return $this; | ||
} | ||
|
||
public function __toString(): string | ||
{ | ||
return \json_encode($this->decoded(), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR); | ||
|
@@ -33,6 +49,60 @@ public function assertMatches(string $expression, $expected): self | |
return $this; | ||
} | ||
|
||
/** | ||
* @param string $selector JMESPath selector | ||
*/ | ||
public function assertHas(string $selector): self | ||
{ | ||
Assert::try(fn() => $this->search("length({$selector})")); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* @param string $selector JMESPath selector | ||
*/ | ||
public function assertMissing(string $selector): self | ||
{ | ||
try { | ||
$this->search("length({$selector})"); | ||
} catch (\RuntimeException $e) { | ||
Assert::pass(); | ||
|
||
return $this; | ||
} | ||
|
||
Assert::fail("Selector \"{$selector}\" does exists."); | ||
} | ||
|
||
/** | ||
* @param callable(Json):mixed $assert | ||
*/ | ||
public function assertThat(string $selector, callable $assert): self | ||
{ | ||
$assert(self::encode($this->search($selector))); | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* @param callable(Json):mixed $assert | ||
*/ | ||
public function assertThatEach(string $selector, callable $assert): void | ||
{ | ||
$value = $this->search($selector); | ||
|
||
if (!\is_array($value)) { | ||
Assert::fail("Value for selector \"{$selector}\" is not an array."); | ||
} | ||
|
||
Assert::that($value)->isNotEmpty(); | ||
|
||
foreach ($value as $item) { | ||
$assert(self::encode($item)); | ||
} | ||
} | ||
|
||
/** | ||
* @return mixed | ||
*/ | ||
|
@@ -56,4 +126,12 @@ public function decoded() | |
|
||
return \json_decode($this->source, true, 512, \JSON_THROW_ON_ERROR); | ||
} | ||
|
||
/** | ||
* @param mixed $data | ||
*/ | ||
private static function encode($data): self | ||
{ | ||
return new self(\json_encode($data, \JSON_THROW_ON_ERROR)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Zenstruck\Browser\Tests; | ||
|
||
use PHPUnit\Framework\AssertionFailedError; | ||
use PHPUnit\Framework\TestCase; | ||
use Zenstruck\Assert; | ||
use Zenstruck\Browser\Json; | ||
|
||
class JsonTest extends TestCase | ||
{ | ||
/** | ||
* @test | ||
* @dataProvider selectorExistsProvider | ||
*/ | ||
public function assert_has_passes_if_selector_exists(string $json, string $selector): void | ||
{ | ||
try { | ||
(new Json($json))->assertHas($selector); | ||
} catch (AssertionFailedError $exception) { | ||
Assert::fail("assertHas() did not assert that selector \"{$selector}\" exists."); | ||
} | ||
} | ||
|
||
/** | ||
* @test | ||
* @dataProvider selectorDoesNotExistProvider | ||
*/ | ||
public function assert_has_fails_if_selector_does_not_exist(string $json, string $selector): void | ||
{ | ||
try { | ||
(new Json($json))->assertHas($selector); | ||
} catch (AssertionFailedError $exception) { | ||
Assert::pass(); | ||
|
||
return; | ||
} | ||
|
||
Assert::fail("assertHas() asserted that selector \"{$selector}\" exists although it does not."); | ||
} | ||
|
||
/** | ||
* @test | ||
* @dataProvider selectorDoesNotExistProvider | ||
*/ | ||
public function assert_missing_passes_if_selector_does_not_exist(string $json, string $selector): void | ||
{ | ||
try { | ||
(new Json($json))->assertMissing($selector); | ||
} catch (AssertionFailedError $exception) { | ||
Assert::fail("assertMissing() asserted that selector \"{$selector}\" is missing although it does not."); | ||
} | ||
} | ||
|
||
/** | ||
* @test | ||
* @dataProvider selectorExistsProvider | ||
*/ | ||
public function assert_missing_fails_if_selector_exists(string $json, string $selector): void | ||
{ | ||
try { | ||
(new Json($json))->assertMissing($selector); | ||
} catch (AssertionFailedError $exception) { | ||
Assert::pass(); | ||
|
||
return; | ||
} | ||
|
||
Assert::fail("assertMissing() did not assert that \"{$selector}\" is missing."); | ||
} | ||
|
||
public function selectorExistsProvider(): iterable | ||
{ | ||
yield ['{"foo":"bar"}', 'foo']; | ||
yield ['{"foo":{"bar": "baz"}}', 'foo.bar']; | ||
yield ['[{"foo":"bar"}]', '[0].foo']; | ||
yield ['{"foo":[{"bar": "baz"}]}', 'foo[0].bar']; | ||
} | ||
|
||
public function selectorDoesNotExistProvider(): iterable | ||
{ | ||
yield ['{}', 'foo']; | ||
yield ['{"foo":"bar"}', 'bar']; | ||
yield ['{"foo":{"bar": "baz"}}', 'foo.baz']; | ||
} | ||
|
||
/** | ||
* @test | ||
* @dataProvider selectHasCountProvider | ||
*/ | ||
public function can_assert_a_selector_has_count(string $json, int $expectedCount): void | ||
{ | ||
try { | ||
(new Json($json))->hasCount($expectedCount); | ||
} catch (AssertionFailedError $exception) { | ||
Assert::fail("assertCount() did not assert that json matches expected count \"{$expectedCount}\""); | ||
} | ||
} | ||
|
||
public function selectHasCountProvider(): iterable | ||
{ | ||
yield ['[]', 0]; | ||
yield ['[1,2,3]', 3]; | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function can_perform_assertions_on_itself(): void | ||
{ | ||
(new Json('["foo","bar"]'))->contains('bar')->doesNotContain('food'); | ||
} | ||
|
||
/** | ||
* @test | ||
* @dataProvider scalarChildAssertionProvider | ||
*/ | ||
public function can_perform_assertion_on_scalar_child(string $selector, callable $asserter): void | ||
{ | ||
(new Json('{"foo":{"bar":"baz"}}'))->assertThat($selector, $asserter); | ||
} | ||
|
||
public function scalarChildAssertionProvider(): iterable | ||
{ | ||
yield ['noop', function(Json $json) {$json->isNull(); }]; | ||
yield ['foo.bar', function(Json $json) {$json->isNotEmpty()->equals('baz'); }]; | ||
} | ||
|
||
/** | ||
* @test | ||
* @dataProvider arrayChildAssertionProvider | ||
*/ | ||
public function can_perform_assertion_on_array_child(string $json, string $selector, callable $asserter): void | ||
{ | ||
(new Json($json))->assertThatEach($selector, $asserter); | ||
} | ||
|
||
public function arrayChildAssertionProvider(): iterable | ||
{ | ||
yield ['{"foo":[1, 2]}', 'foo', function(Json $json) {$json->isGreaterThan(0); }]; | ||
yield ['{"foo":[{"bar": 1}, {"bar": 2}]}', 'foo[*].bar', function(Json $json) {$json->isGreaterThan(0); }]; | ||
} | ||
|
||
/** | ||
* @test | ||
* @dataProvider invalidArrayChildAssertionProvider | ||
*/ | ||
public function assert_that_each_throws_if_invalid_array_given(string $json, string $selector, callable $asserter): void | ||
{ | ||
try { | ||
(new Json($json))->assertThatEach($selector, $asserter); | ||
} catch (AssertionFailedError $e) { | ||
Assert::pass(); | ||
|
||
return; | ||
} | ||
|
||
Assert::fail('Json::assertThatEach() should raise exception with invalid arrays.'); | ||
} | ||
|
||
public function invalidArrayChildAssertionProvider(): iterable | ||
{ | ||
yield ['{}', 'foo', static function(Json $json) {}]; | ||
yield ['{"foo": "bar"}', 'foo', static function(Json $json) {}]; | ||
yield ['{"foo": []}', 'foo', static function(Json $json) {}]; | ||
} | ||
} |