From ba649d68e386b308e0518f33b73ad0fd85c748b4 Mon Sep 17 00:00:00 2001 From: Adam Goulding Date: Tue, 11 Aug 2026 16:12:46 +0100 Subject: [PATCH] fix: beforeEach/afterEach leaking into sibling describe blocks with same nested name --- src/PendingCalls/AfterEachCall.php | 3 +-- src/PendingCalls/BeforeEachCall.php | 11 ++++------- tests/.snapshots/success.txt | 8 +++++++- tests/Features/AfterEach.php | 30 +++++++++++++++++++++++++++++ tests/Features/BeforeEach.php | 26 +++++++++++++++++++++++++ tests/Features/Describe.php | 14 ++++++++++++++ tests/Visual/Parallel.php | 4 ++-- 7 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/PendingCalls/AfterEachCall.php b/src/PendingCalls/AfterEachCall.php index 9f7f0b558..6c0ed4ea5 100644 --- a/src/PendingCalls/AfterEachCall.php +++ b/src/PendingCalls/AfterEachCall.php @@ -6,7 +6,6 @@ use Closure; use Pest\PendingCalls\Concerns\Describable; -use Pest\Support\Arr; use Pest\Support\Backtrace; use Pest\Support\ChainableClosure; use Pest\Support\HigherOrderMessageCollection; @@ -43,7 +42,7 @@ public function __destruct() $proxies = $this->proxies; $afterEachTestCase = ChainableClosure::boundWhen( - fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true), + fn (): bool => $describing === [] || (count($describing) <= count($this->__describing) && array_slice($this->__describing, 0, count($describing)) === $describing), ChainableClosure::bound(fn () => $proxies->chain($this), $this->closure)->bindTo($this, self::class), )->bindTo($this, self::class); diff --git a/src/PendingCalls/BeforeEachCall.php b/src/PendingCalls/BeforeEachCall.php index 852a1aad5..34f08f3f0 100644 --- a/src/PendingCalls/BeforeEachCall.php +++ b/src/PendingCalls/BeforeEachCall.php @@ -7,7 +7,6 @@ use Closure; use Pest\Exceptions\AfterBeforeTestFunction; use Pest\PendingCalls\Concerns\Describable; -use Pest\Support\Arr; use Pest\Support\Backtrace; use Pest\Support\ChainableClosure; use Pest\Support\HigherOrderMessageCollection; @@ -50,11 +49,9 @@ public function __destruct() $beforeEachTestCall = function (TestCall $testCall) use ($describing): void { if ($this->describing !== []) { - if (Arr::last($describing) !== Arr::last($this->describing)) { - return; - } - - if (! in_array(Arr::last($describing), $testCall->describing, true)) { + $testCallDescribing = $testCall->describing; + if (count($describing) > count($testCallDescribing) || + array_slice($testCallDescribing, 0, count($describing)) !== $describing) { return; } } @@ -63,7 +60,7 @@ public function __destruct() }; $beforeEachTestCase = ChainableClosure::boundWhen( - fn (): bool => $describing === [] || in_array(Arr::last($describing), $this->__describing, true), + fn (): bool => $describing === [] || (count($describing) <= count($this->__describing) && array_slice($this->__describing, 0, count($describing)) === $describing), ChainableClosure::bound(fn () => $testCaseProxies->chain($this), $this->closure)->bindTo($this, self::class), )->bindTo($this, self::class); diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index c6bc6212b..b88ae8c1e 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -33,6 +33,8 @@ ✓ matching describe block names → outer → middle → it should not call afterEach functions for sibling describe blocks with the same name ✓ matching describe block names → outer → inner → it does not get executed before the test ✓ matching describe block names → outer → inner → it should not call afterEach functions for descendent of sibling describe blocks with the same name + ✓ hierarchical test naming → block one → the same name → it does not call afterEach from sibling describe with same name + ✓ hierarchical test naming → block two → the same name → it correctly calls afterEach from own describe hierarchy PASS Tests\Features\Assignee ✓ it may be associated with an assignee [@nunomaduro, @taylorotwell] @@ -56,6 +58,8 @@ ✓ matching name → it should not call the before each on the describe block with the same name ✓ called on all tests → beforeEach should be called ✓ called on all tests → beforeEach should be called for all tests + ✓ hierarchical test naming → block one → the same name → it does not call beforeEach from sibling describe with same name + ✓ hierarchical test naming → block two → the same name → it correctly calls beforeEach from own describe hierarchy PASS Tests\Features\BeforeEachProxiesToTestCallWithExpectations ✓ runs 1 @@ -320,6 +324,8 @@ ✓ depends on describe using with → foo with (3) ✓ depends on describe using with → bar with (3) ✓ with test after describe → it should run the before each + ✓ sibling describes may share the same it() description → block one → it can be created + ✓ sibling describes may share the same it() description → block two → it can be created PASS Tests\Features\DescriptionLess ✓ get 'foo' @@ -2221,4 +2227,4 @@ ✓ pass with dataset with ('my-datas-set-value') ✓ within describe → pass with dataset with ('my-datas-set-value') - Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1570 passed (3410 assertions) \ No newline at end of file + Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1576 passed (3417 assertions) \ No newline at end of file diff --git a/tests/Features/AfterEach.php b/tests/Features/AfterEach.php index 21b975eb3..afbd89a65 100644 --- a/tests/Features/AfterEach.php +++ b/tests/Features/AfterEach.php @@ -100,3 +100,33 @@ }); }); }); + +describe('hierarchical test naming', function (): void { + describe('block one', function (): void { + describe('the same name', function (): void { + it('does not call afterEach from sibling describe with same name', function (): void { + expect($this)->not->toHaveProperty('example'); + }); + }); + }); + + describe('block two', function (): void { + afterEach(function (): void { + expect($this->result)->toBeFalse(); + }); + + describe('the same name', function (): void { + beforeEach(function (): void { + $this->example = false; + }); + + afterEach(function (): void { + $this->result = $this->example; + }); + + it('correctly calls afterEach from own describe hierarchy', function (): void { + expect($this->example)->toBeFalse(); + }); + }); + }); +}); diff --git a/tests/Features/BeforeEach.php b/tests/Features/BeforeEach.php index ef78194e9..e4832d97f 100644 --- a/tests/Features/BeforeEach.php +++ b/tests/Features/BeforeEach.php @@ -126,3 +126,29 @@ expect($this->baz)->toBe(2); }); }); + +describe('hierarchical test naming', function (): void { + describe('block one', function (): void { + describe('the same name', function (): void { + it('does not call beforeEach from sibling describe with same name', function (): void { + expect($this)->not->toHaveProperty('example'); + }); + }); + }); + + describe('block two', function (): void { + beforeEach(function (): void { + $this->example = false; + }); + + describe('the same name', function (): void { + beforeEach(function (): void { + $this->result = $this->example; + }); + + it('correctly calls beforeEach from own describe hierarchy', function (): void { + expect($this->result)->toBeFalse(); + }); + }); + }); +}); diff --git a/tests/Features/Describe.php b/tests/Features/Describe.php index f3fc130d8..ed5de20d8 100644 --- a/tests/Features/Describe.php +++ b/tests/Features/Describe.php @@ -108,3 +108,17 @@ expect($this->count)->toBe(2); }); }); + +describe('sibling describes may share the same it() description', function (): void { + describe('block one', function (): void { + it('can be created', function (): void { + expect(true)->toBeTrue(); + }); + }); + + describe('block two', function (): void { + it('can be created', function (): void { + expect(true)->toBeTrue(); + }); + }); +}); diff --git a/tests/Visual/Parallel.php b/tests/Visual/Parallel.php index 6bb90a0ae..fd34ad4ca 100644 --- a/tests/Visual/Parallel.php +++ b/tests/Visual/Parallel.php @@ -26,13 +26,13 @@ $file = file_get_contents(__FILE__); $file = preg_replace( '/\$expected = \'.*?\';/', - "\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1552 passed (3355 assertions)';", + "\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1558 passed (3362 assertions)';", $file, ); file_put_contents(__FILE__, $file); } - $expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1552 passed (3355 assertions)'; + $expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1558 passed (3362 assertions)'; expect($output) ->toContain("Tests: {$expected}")