Skip to content

Commit

Permalink
[feature] add Json::assertHas()/assertMissing()/assertThat()/`a…
Browse files Browse the repository at this point in the history
…ssertThatEach()` (#92)
  • Loading branch information
nikophil authored Jun 29, 2022
1 parent 7efe89e commit 90393a9
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 2 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ $json = $browser
;

$json->assertMatches('foo.bar.baz', 1);
$json->assertHas('foo.bar.baz');
$json->assertMissing('foo.bar.boo');
$json->search('foo.bar.baz'); // mixed (the found value at "JMESPath expression")
$json->decoded(); // the decoded json
(string) $json; // the json string pretty-printed
Expand All @@ -422,7 +424,15 @@ $json->decoded(); // the decoded json
$json = $browser
->get('/api/endpoint')
->use(function(\Zenstruck\Browser\Json $json) {
$json->assertMatches('foo.bar.baz', 1);
// Json acts like a proxy of zenstruck/assert Expectation class
$json->hasCount(5);
$json->contains('foo');
// assert on children: the closure gets Json object contextualized on given selector
// {"foo": "bar"}
$json->assertThat('foo', fn(Json $json) => $json->equals('bar'))
// assert on each element of an array
// {"foo": [1, 2, 3]}
$json->assertThatEach('foo', fn(Json $json) => $json->isGreaterThan(0));
})
;
```
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"symfony/dom-crawler": "^5.4|^6.0",
"symfony/framework-bundle": "^5.4|^6.0",
"symfony/polyfill-php80": "^1.20",
"zenstruck/assert": "^1.0",
"zenstruck/assert": "^1.1",
"zenstruck/callback": "^1.4.2"
},
"require-dev": {
Expand Down
78 changes: 78 additions & 0 deletions src/Browser/Json.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
Expand All @@ -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
*/
Expand All @@ -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));
}
}
169 changes: 169 additions & 0 deletions tests/JsonTest.php
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) {}];
}
}

0 comments on commit 90393a9

Please sign in to comment.