Skip to content

Commit a7ec58e

Browse files
[8.x] Adjust Fluent Assertions (#36620)
* Adjust Fluent Assertions - Add `first` scoping method - Allow `has(3)` to count the current scope - Expose `count` method publicly * Skip interaction check when top-level is non-associative * Test: Remove redundant `etc` call * Fix broken test
1 parent 354c57b commit a7ec58e

File tree

6 files changed

+242
-36
lines changed

6 files changed

+242
-36
lines changed

src/Illuminate/Testing/Fluent/AssertableJson.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ protected function __construct(array $props, string $path = null)
5252
* @param string $key
5353
* @return string
5454
*/
55-
protected function dotPath(string $key): string
55+
protected function dotPath(string $key = ''): string
5656
{
5757
if (is_null($this->path)) {
5858
return $key;
5959
}
6060

61-
return implode('.', [$this->path, $key]);
61+
return rtrim(implode('.', [$this->path, $key]), '.');
6262
}
6363

6464
/**
@@ -93,6 +93,30 @@ protected function scope(string $key, Closure $callback): self
9393
return $this;
9494
}
9595

96+
/**
97+
* Instantiate a new "scope" on the first child element.
98+
*
99+
* @param \Closure $callback
100+
* @return $this
101+
*/
102+
public function first(Closure $callback): self
103+
{
104+
$props = $this->prop();
105+
106+
$path = $this->dotPath();
107+
108+
PHPUnit::assertNotEmpty($props, $path === ''
109+
? 'Cannot scope directly onto the first element of the root level because it is empty.'
110+
: sprintf('Cannot scope directly onto the first element of property [%s] because it is empty.', $path)
111+
);
112+
113+
$key = array_keys($props)[0];
114+
115+
$this->interactsWith($key);
116+
117+
return $this->scope($key, $callback);
118+
}
119+
96120
/**
97121
* Create a new instance from an array.
98122
*

src/Illuminate/Testing/Fluent/Concerns/Has.php

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,26 @@ trait Has
1111
/**
1212
* Assert that the prop is of the expected size.
1313
*
14-
* @param string $key
15-
* @param int $length
14+
* @param string|int $key
15+
* @param int|null $length
1616
* @return $this
1717
*/
18-
protected function count(string $key, int $length): self
18+
public function count($key, int $length = null): self
1919
{
20+
if (is_null($length)) {
21+
$path = $this->dotPath();
22+
23+
PHPUnit::assertCount(
24+
$key,
25+
$this->prop(),
26+
$path
27+
? sprintf('Property [%s] does not have the expected size.', $path)
28+
: sprintf('Root level does not have the expected size.')
29+
);
30+
31+
return $this;
32+
}
33+
2034
PHPUnit::assertCount(
2135
$length,
2236
$this->prop($key),
@@ -29,41 +43,40 @@ protected function count(string $key, int $length): self
2943
/**
3044
* Ensure that the given prop exists.
3145
*
32-
* @param string $key
33-
* @param null $value
34-
* @param \Closure|null $scope
46+
* @param string|int $key
47+
* @param int|\Closure|null $length
48+
* @param \Closure|null $callback
3549
* @return $this
3650
*/
37-
public function has(string $key, $value = null, Closure $scope = null): self
51+
public function has($key, $length = null, Closure $callback = null): self
3852
{
3953
$prop = $this->prop();
4054

55+
if (is_int($key) && is_null($length)) {
56+
return $this->count($key);
57+
}
58+
4159
PHPUnit::assertTrue(
4260
Arr::has($prop, $key),
4361
sprintf('Property [%s] does not exist.', $this->dotPath($key))
4462
);
4563

4664
$this->interactsWith($key);
4765

48-
// When all three arguments are provided this indicates a short-hand expression
49-
// that combines both a `count`-assertion, followed by directly creating the
50-
// `scope` on the first element. We can simply handle this correctly here.
51-
if (is_int($value) && ! is_null($scope)) {
52-
$prop = $this->prop($key);
53-
$path = $this->dotPath($key);
54-
55-
PHPUnit::assertTrue($value > 0, sprintf('Cannot scope directly onto the first entry of property [%s] when asserting that it has a size of 0.', $path));
56-
PHPUnit::assertIsArray($prop, sprintf('Direct scoping is unsupported for non-array like properties such as [%s].', $path));
57-
58-
$this->count($key, $value);
66+
if (is_int($length) && ! is_null($callback)) {
67+
return $this->has($key, function (self $scope) use ($length, $callback) {
68+
return $scope->count($length)
69+
->first($callback)
70+
->etc();
71+
});
72+
}
5973

60-
return $this->scope($key.'.'.array_keys($prop)[0], $scope);
74+
if (is_callable($length)) {
75+
return $this->scope($key, $length);
6176
}
6277

63-
if (is_callable($value)) {
64-
$this->scope($key, $value);
65-
} elseif (! is_null($value)) {
66-
$this->count($key, $value);
78+
if (! is_null($length)) {
79+
return $this->count($key, $length);
6780
}
6881

6982
return $this;
@@ -129,7 +142,7 @@ public function missing(string $key): self
129142
* @param string $key
130143
* @return string
131144
*/
132-
abstract protected function dotPath(string $key): string;
145+
abstract protected function dotPath(string $key = ''): string;
133146

134147
/**
135148
* Marks the property as interacted.
@@ -155,4 +168,19 @@ abstract protected function prop(string $key = null);
155168
* @return $this
156169
*/
157170
abstract protected function scope(string $key, Closure $callback);
171+
172+
/**
173+
* Disables the interaction check.
174+
*
175+
* @return $this
176+
*/
177+
abstract public function etc();
178+
179+
/**
180+
* Instantiate a new "scope" on the first element.
181+
*
182+
* @param \Closure $callback
183+
* @return $this
184+
*/
185+
abstract public function first(Closure $callback);
158186
}

src/Illuminate/Testing/Fluent/Concerns/Matching.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ protected function ensureSorted(&$value): void
8787
* @param string $key
8888
* @return string
8989
*/
90-
abstract protected function dotPath(string $key): string;
90+
abstract protected function dotPath(string $key = ''): string;
9191

9292
/**
9393
* Ensure that the given prop exists.

src/Illuminate/Testing/TestResponse.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ public function assertJson($value, $strict = false)
523523

524524
$value($assert);
525525

526-
if ($strict) {
526+
if (Arr::isAssoc($assert->toArray())) {
527527
$assert->interacted();
528528
}
529529
}

tests/Testing/Fluent/AssertTest.php

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public function testAssertHasFailsWhenNestedPropMissing()
5858
$assert->has('example.another');
5959
}
6060

61-
public function testAssertCountItemsInProp()
61+
public function testAssertHasCountItemsInProp()
6262
{
6363
$assert = AssertableJson::fromArray([
6464
'bar' => [
@@ -70,7 +70,7 @@ public function testAssertCountItemsInProp()
7070
$assert->has('bar', 2);
7171
}
7272

73-
public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch()
73+
public function testAssertHasCountFailsWhenAmountOfItemsDoesNotMatch()
7474
{
7575
$assert = AssertableJson::fromArray([
7676
'bar' => [
@@ -85,7 +85,7 @@ public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch()
8585
$assert->has('bar', 1);
8686
}
8787

88-
public function testAssertCountFailsWhenPropMissing()
88+
public function testAssertHasCountFailsWhenPropMissing()
8989
{
9090
$assert = AssertableJson::fromArray([
9191
'bar' => [
@@ -111,6 +111,90 @@ public function testAssertHasFailsWhenSecondArgumentUnsupportedType()
111111
$assert->has('bar', 'invalid');
112112
}
113113

114+
public function testAssertHasOnlyCounts()
115+
{
116+
$assert = AssertableJson::fromArray([
117+
'foo',
118+
'bar',
119+
'baz',
120+
]);
121+
122+
$assert->has(3);
123+
}
124+
125+
public function testAssertHasOnlyCountFails()
126+
{
127+
$assert = AssertableJson::fromArray([
128+
'foo',
129+
'bar',
130+
'baz',
131+
]);
132+
133+
$this->expectException(AssertionFailedError::class);
134+
$this->expectExceptionMessage('Root level does not have the expected size.');
135+
136+
$assert->has(2);
137+
}
138+
139+
public function testAssertHasOnlyCountFailsScoped()
140+
{
141+
$assert = AssertableJson::fromArray([
142+
'bar' => [
143+
'baz' => 'example',
144+
'prop' => 'value',
145+
],
146+
]);
147+
148+
$this->expectException(AssertionFailedError::class);
149+
$this->expectExceptionMessage('Property [bar] does not have the expected size.');
150+
151+
$assert->has('bar', function ($bar) {
152+
$bar->has(3);
153+
});
154+
}
155+
156+
public function testAssertCount()
157+
{
158+
$assert = AssertableJson::fromArray([
159+
'foo',
160+
'bar',
161+
'baz',
162+
]);
163+
164+
$assert->count(3);
165+
}
166+
167+
public function testAssertCountFails()
168+
{
169+
$assert = AssertableJson::fromArray([
170+
'foo',
171+
'bar',
172+
'baz',
173+
]);
174+
175+
$this->expectException(AssertionFailedError::class);
176+
$this->expectExceptionMessage('Root level does not have the expected size.');
177+
178+
$assert->count(2);
179+
}
180+
181+
public function testAssertCountFailsScoped()
182+
{
183+
$assert = AssertableJson::fromArray([
184+
'bar' => [
185+
'baz' => 'example',
186+
'prop' => 'value',
187+
],
188+
]);
189+
190+
$this->expectException(AssertionFailedError::class);
191+
$this->expectExceptionMessage('Property [bar] does not have the expected size.');
192+
193+
$assert->has('bar', function ($bar) {
194+
$bar->count(3);
195+
});
196+
}
197+
114198
public function testAssertMissing()
115199
{
116200
$assert = AssertableJson::fromArray([
@@ -421,7 +505,7 @@ public function testScopeShorthandFailsWhenAssertingZeroItems()
421505
]);
422506

423507
$this->expectException(AssertionFailedError::class);
424-
$this->expectExceptionMessage('Cannot scope directly onto the first entry of property [bar] when asserting that it has a size of 0.');
508+
$this->expectExceptionMessage('Property [bar] does not have the expected size.');
425509

426510
$assert->has('bar', 0, function (AssertableJson $item) {
427511
$item->where('key', 'first');
@@ -445,6 +529,64 @@ public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch()
445529
});
446530
}
447531

532+
public function testFirstScope()
533+
{
534+
$assert = AssertableJson::fromArray([
535+
'foo' => [
536+
'key' => 'first',
537+
],
538+
'bar' => [
539+
'key' => 'second',
540+
],
541+
]);
542+
543+
$assert->first(function (AssertableJson $item) {
544+
$item->where('key', 'first');
545+
});
546+
}
547+
548+
public function testFirstScopeFailsWhenNoProps()
549+
{
550+
$assert = AssertableJson::fromArray([]);
551+
552+
$this->expectException(AssertionFailedError::class);
553+
$this->expectExceptionMessage('Cannot scope directly onto the first element of the root level because it is empty.');
554+
555+
$assert->first(function (AssertableJson $item) {
556+
//
557+
});
558+
}
559+
560+
public function testFirstNestedScopeFailsWhenNoProps()
561+
{
562+
$assert = AssertableJson::fromArray([
563+
'foo' => [],
564+
]);
565+
566+
$this->expectException(AssertionFailedError::class);
567+
$this->expectExceptionMessage('Cannot scope directly onto the first element of property [foo] because it is empty.');
568+
569+
$assert->has('foo', function (AssertableJson $assert) {
570+
$assert->first(function (AssertableJson $item) {
571+
//
572+
});
573+
});
574+
}
575+
576+
public function testFirstScopeFailsWhenPropSingleValue()
577+
{
578+
$assert = AssertableJson::fromArray([
579+
'foo' => 'bar',
580+
]);
581+
582+
$this->expectException(AssertionFailedError::class);
583+
$this->expectExceptionMessage('Property [foo] is not scopeable.');
584+
585+
$assert->first(function (AssertableJson $item) {
586+
//
587+
});
588+
}
589+
448590
public function testFailsWhenNotInteractingWithAllPropsInScope()
449591
{
450592
$assert = AssertableJson::fromArray([

0 commit comments

Comments
 (0)