From fa0e11cf973cd0ea08ee25ab002a7be482195c37 Mon Sep 17 00:00:00 2001 From: Jake Hotson Date: Fri, 21 Mar 2025 23:02:44 +0000 Subject: [PATCH 1/4] [TASK] Add trait providing standard implementation of `Commentable` Part of #813. --- composer.json | 1 + src/Comment/CommentContainer.php | 42 +++ src/Comment/Commentable.php | 3 + tests/Unit/Comment/CommentContainerTest.php | 239 ++++++++++++++++++ .../Fixtures/ConcreteCommentContainer.php | 13 + 5 files changed, 298 insertions(+) create mode 100644 src/Comment/CommentContainer.php create mode 100644 tests/Unit/Comment/CommentContainerTest.php create mode 100644 tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php diff --git a/composer.json b/composer.json index f07d3222..192eea6d 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "phpstan/phpstan-phpunit": "1.4.2 || 2.0.4", "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.3", "phpunit/phpunit": "8.5.41", + "rawr/cross-data-providers": "2.4.0", "rector/rector": "1.2.10 || 2.0.7", "rector/type-perfect": "1.0.0 || 2.0.2" }, diff --git a/src/Comment/CommentContainer.php b/src/Comment/CommentContainer.php new file mode 100644 index 00000000..7d7b3b69 --- /dev/null +++ b/src/Comment/CommentContainer.php @@ -0,0 +1,42 @@ + + */ + protected $comments = []; + + /** + * @param list $comments + */ + public function addComments(array $comments): void + { + $this->comments = \array_merge($this->comments, $comments); + } + + /** + * @return list + */ + public function getComments(): array + { + return $this->comments; + } + + /** + * @param list $comments + */ + public function setComments(array $comments): void + { + $this->comments = $comments; + } +} diff --git a/src/Comment/Commentable.php b/src/Comment/Commentable.php index e6eb6a0b..5f28021d 100644 --- a/src/Comment/Commentable.php +++ b/src/Comment/Commentable.php @@ -4,6 +4,9 @@ namespace Sabberworm\CSS\Comment; +/** + * A standard implementation of this interface is available in the `CommentContainer` trait. + */ interface Commentable { /** diff --git a/tests/Unit/Comment/CommentContainerTest.php b/tests/Unit/Comment/CommentContainerTest.php new file mode 100644 index 00000000..16173505 --- /dev/null +++ b/tests/Unit/Comment/CommentContainerTest.php @@ -0,0 +1,239 @@ +subject = new ConcreteCommentContainer(); + } + + /** + * @test + */ + public function getCommentsInitiallyReturnsEmptyArray(): void + { + self::assertSame([], $this->subject->getComments()); + } + + /** + * @return array}> + */ + public function provideCommentArray(): array + { + return [ + 'no comment' => [[]], + 'one comment' => [[new Comment('Is this really a spoon?')]], + 'two comments' => [[ + new Comment('I’m a teapot.'), + new Comment('I’m a cafetière.'), + ]], + ]; + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function getCommentsAfterCommentsAddedToVirginContainerReturnsThoseComments(array $comments): void + { + $this->subject->addComments($comments); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function getCommentsAfterEmptyArrayOfCommentsAddedReturnsOriginalComments(array $comments): void + { + $this->subject->setComments($comments); + + $this->subject->addComments([]); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @return array}> + */ + public function provideAlternativeCommentArray(): array + { + return [ + 'no comment' => [[]], + 'one comment' => [[new Comment('Can I eat it with my hands?')]], + 'two comments' => [[ + new Comment('I’m a beer barrel.'), + new Comment('I’m a vineyard.'), + ]], + ]; + } + + /** + * @return array}> + */ + public function provideAlternativeNonemptyCommentArray(): array + { + $data = $this->provideAlternativeCommentArray(); + + unset($data['no comment']); + + return $data; + } + + /** + * This provider crosses two comment arrays (0, 1 or 2 comments) with different comments, + * so that all combinations can be tested. + * + * @return array, 1: list}> + */ + public function provideTwoDistinctCommentArrays(): array + { + return DataProviders::cross($this->provideCommentArray(), $this->provideAlternativeCommentArray()); + } + + /** + * @return array, 1: non-empty-list}> + */ + public function provideTwoDistinctCommentArraysWithSecondNonempty(): array + { + return DataProviders::cross($this->provideCommentArray(), $this->provideAlternativeNonemptyCommentArray()); + } + + private static function createContainsContstraint(Comment $comment): TraversableContains + { + return new TraversableContains($comment); + } + + /** + * @param non-empty-list $comments + * + * @return non-empty-list + */ + private static function createContainsContstraints(array $comments): array + { + return \array_map([self::class, 'createContainsContstraint'], $comments); + } + + /** + * @test + * + * @param list $commentsToAdd + * @param non-empty-list $originalComments + * + * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty + */ + public function getCommentsAfterCommentsAddedIncludesOriginalComments( + array $commentsToAdd, + array $originalComments + ): void { + $this->subject->setComments($originalComments); + + $this->subject->addComments($commentsToAdd); + + self::assertThat( + $this->subject->getComments(), + LogicalAnd::fromConstraints(...self::createContainsContstraints($originalComments)) + ); + } + + /** + * @test + * + * @param list $originalComments + * @param non-empty-list $commentsToAdd + * + * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty + */ + public function getCommentsAfterCommentsAddedIncludesCommentsAdded( + array $originalComments, + array $commentsToAdd + ): void { + $this->subject->setComments($originalComments); + + $this->subject->addComments($commentsToAdd); + + self::assertThat( + $this->subject->getComments(), + LogicalAnd::fromConstraints(...self::createContainsContstraints($commentsToAdd)) + ); + } + + /** + * @test + * + * @param non-empty-list $comments + * + * @dataProvider provideAlternativeNonemptyCommentArray + */ + public function addCommentsAppends(array $comments): void + { + $firstComment = new Comment('I must be first!'); + $this->subject->setComments([$firstComment]); + + $this->subject->addComments($comments); + + $result = $this->subject->getComments(); + self::assertNotEmpty($result); + self::assertSame($firstComment, $result[0]); + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function getCommentsAfterCommentsSetOnVirginContainerReturnsThoseComments(array $comments): void + { + $this->subject->setComments($comments); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @test + * + * @param list $originalComments + * @param list $commentsToSet + * + * @dataProvider provideTwoDistinctCommentArrays + */ + public function getCommentsAfterCommentsSetOnContainerWithCommentsReturnsOnlyCommentsSet( + array $originalComments, + array $commentsToSet + ): void { + $this->subject->setComments($originalComments); + + $this->subject->setComments($commentsToSet); + + self::assertSame($commentsToSet, $this->subject->getComments()); + } +} diff --git a/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php b/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php new file mode 100644 index 00000000..39f6ec37 --- /dev/null +++ b/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php @@ -0,0 +1,13 @@ + Date: Sat, 22 Mar 2025 19:41:00 +0000 Subject: [PATCH 2/4] With PHPStan, require trait users to implement `Commentable` --- src/Comment/CommentContainer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Comment/CommentContainer.php b/src/Comment/CommentContainer.php index 7d7b3b69..87f6ff46 100644 --- a/src/Comment/CommentContainer.php +++ b/src/Comment/CommentContainer.php @@ -8,6 +8,8 @@ * Provides a standard reusable implementation of `Commentable`. * * @internal + * + * @phpstan-require-implements Commentable */ trait CommentContainer { From 94a17b41bf08e32a44bf0eb83d0dbc9c583c3eb0 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Tue, 25 Mar 2025 01:39:09 +0000 Subject: [PATCH 3/4] Change test subject type to `ConcreteCommentContainer` Co-authored-by: Oliver Klee --- tests/Unit/Comment/CommentContainerTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Unit/Comment/CommentContainerTest.php b/tests/Unit/Comment/CommentContainerTest.php index 16173505..bdb1ad27 100644 --- a/tests/Unit/Comment/CommentContainerTest.php +++ b/tests/Unit/Comment/CommentContainerTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\Constraint\TraversableContains; use PHPUnit\Framework\TestCase; use Sabberworm\CSS\Comment\Comment; -use Sabberworm\CSS\Comment\Commentable; use Sabberworm\CSS\Tests\Unit\Comment\Fixtures\ConcreteCommentContainer; use TRegx\DataProvider\DataProviders; @@ -18,7 +17,7 @@ final class CommentContainerTest extends TestCase { /** - * @var Commentable + * @var ConcreteCommentContainer */ private $subject; From 65bb1e7b65396c9f17d5b29458ec3cfc9ddd6fa7 Mon Sep 17 00:00:00 2001 From: Jake Hotson Date: Tue, 25 Mar 2025 19:20:05 +0000 Subject: [PATCH 4/4] Rename test methods to focus on the main method under test In most cases this is either `addComments` or `setComments`, rather than `getComments`. --- tests/Unit/Comment/CommentContainerTest.php | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/tests/Unit/Comment/CommentContainerTest.php b/tests/Unit/Comment/CommentContainerTest.php index bdb1ad27..a29691b9 100644 --- a/tests/Unit/Comment/CommentContainerTest.php +++ b/tests/Unit/Comment/CommentContainerTest.php @@ -56,7 +56,7 @@ public function provideCommentArray(): array * * @dataProvider provideCommentArray */ - public function getCommentsAfterCommentsAddedToVirginContainerReturnsThoseComments(array $comments): void + public function addCommentsOnVirginContainerAddsCommentsProvided(array $comments): void { $this->subject->addComments($comments); @@ -70,7 +70,7 @@ public function getCommentsAfterCommentsAddedToVirginContainerReturnsThoseCommen * * @dataProvider provideCommentArray */ - public function getCommentsAfterEmptyArrayOfCommentsAddedReturnsOriginalComments(array $comments): void + public function addCommentsWithEmptyArrayKeepsOriginalCommentsUnchanged(array $comments): void { $this->subject->setComments($comments); @@ -148,10 +148,8 @@ private static function createContainsContstraints(array $comments): array * * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty */ - public function getCommentsAfterCommentsAddedIncludesOriginalComments( - array $commentsToAdd, - array $originalComments - ): void { + public function addCommentsKeepsOriginalComments(array $commentsToAdd, array $originalComments): void + { $this->subject->setComments($originalComments); $this->subject->addComments($commentsToAdd); @@ -170,10 +168,8 @@ public function getCommentsAfterCommentsAddedIncludesOriginalComments( * * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty */ - public function getCommentsAfterCommentsAddedIncludesCommentsAdded( - array $originalComments, - array $commentsToAdd - ): void { + public function addCommentsAfterCommentsSetAddsCommentsProvided(array $originalComments, array $commentsToAdd): void + { $this->subject->setComments($originalComments); $this->subject->addComments($commentsToAdd); @@ -210,7 +206,7 @@ public function addCommentsAppends(array $comments): void * * @dataProvider provideCommentArray */ - public function getCommentsAfterCommentsSetOnVirginContainerReturnsThoseComments(array $comments): void + public function setCommentsOnVirginContainerSetsCommentsProvided(array $comments): void { $this->subject->setComments($comments); @@ -225,10 +221,8 @@ public function getCommentsAfterCommentsSetOnVirginContainerReturnsThoseComments * * @dataProvider provideTwoDistinctCommentArrays */ - public function getCommentsAfterCommentsSetOnContainerWithCommentsReturnsOnlyCommentsSet( - array $originalComments, - array $commentsToSet - ): void { + public function setCommentsReplacesWithCommentsProvided(array $originalComments, array $commentsToSet): void + { $this->subject->setComments($originalComments); $this->subject->setComments($commentsToSet);