Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
44 changes: 44 additions & 0 deletions src/Comment/CommentContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Sabberworm\CSS\Comment;

/**
* Provides a standard reusable implementation of `Commentable`.
*
* @internal
*
* @phpstan-require-implements Commentable
*/
trait CommentContainer
{
/**
* @var list<Comment>
*/
protected $comments = [];

/**
* @param list<Comment> $comments
*/
public function addComments(array $comments): void
{
$this->comments = \array_merge($this->comments, $comments);
}

/**
* @return list<Comment>
*/
public function getComments(): array
{
return $this->comments;
}

/**
* @param list<Comment> $comments
*/
public function setComments(array $comments): void
{
$this->comments = $comments;
}
}
3 changes: 3 additions & 0 deletions src/Comment/Commentable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Sabberworm\CSS\Comment;

/**
* A standard implementation of this interface is available in the `CommentContainer` trait.
*/
interface Commentable
{
/**
Expand Down
239 changes: 239 additions & 0 deletions tests/Unit/Comment/CommentContainerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
<?php

declare(strict_types=1);

namespace Sabberworm\CSS\Tests\Unit\Comment;

use PHPUnit\Framework\Constraint\LogicalAnd;
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;

/**
* @covers \Sabberworm\CSS\Comment\CommentContainer
*/
final class CommentContainerTest extends TestCase
{
/**
* @var Commentable
*/
private $subject;

protected function setUp(): void
{
$this->subject = new ConcreteCommentContainer();
}

/**
* @test
*/
public function getCommentsInitiallyReturnsEmptyArray(): void
{
self::assertSame([], $this->subject->getComments());
}

/**
* @return array<non-empty-string, array{0: list<Comment>}>
*/
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<Comment> $comments
*
* @dataProvider provideCommentArray
*/
public function getCommentsAfterCommentsAddedToVirginContainerReturnsThoseComments(array $comments): void
{
$this->subject->addComments($comments);

self::assertSame($comments, $this->subject->getComments());
}

/**
* @test
*
* @param list<Comment> $comments
*
* @dataProvider provideCommentArray
*/
public function getCommentsAfterEmptyArrayOfCommentsAddedReturnsOriginalComments(array $comments): void
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can describe the effects instead of focusing on the methods:

Suggested change
public function getCommentsAfterEmptyArrayOfCommentsAddedReturnsOriginalComments(array $comments): void
public function setCommentsWithEmptyArrayKeepsOriginalCommentsUnchanged(array $comments): void

Copy link
Collaborator Author

@JakeQZ JakeQZ Mar 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from getCommentsInitiallyReturnsEmptyArray(), all the test methods focus on either addComments() (first group) or setComments() (last group). So perhaps they all be renamed like addCommentsDoesXYZ() and setCommentsDoesXYZ - WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'd like that!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

{
$this->subject->setComments($comments);

$this->subject->addComments([]);

self::assertSame($comments, $this->subject->getComments());
}

/**
* @return array<non-empty-string, array{0: list<Comment>}>
*/
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<non-empty-string, array{0: non-empty-list<Comment>}>
*/
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<non-empty-string, array{0: list<Comment>, 1: list<Comment>}>
*/
public function provideTwoDistinctCommentArrays(): array
{
return DataProviders::cross($this->provideCommentArray(), $this->provideAlternativeCommentArray());
}

/**
* @return array<non-empty-string, array{0: list<Comment>, 1: non-empty-list<Comment>}>
*/
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<Comment> $comments
*
* @return non-empty-list<TraversableContains>
*/
private static function createContainsContstraints(array $comments): array
{
return \array_map([self::class, 'createContainsContstraint'], $comments);
}

/**
* @test
*
* @param list<Comment> $commentsToAdd
* @param non-empty-list<Comment> $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<Comment> $originalComments
* @param non-empty-list<Comment> $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<Comment> $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<Comment> $comments
*
* @dataProvider provideCommentArray
*/
public function getCommentsAfterCommentsSetOnVirginContainerReturnsThoseComments(array $comments): void
{
$this->subject->setComments($comments);

self::assertSame($comments, $this->subject->getComments());
}

/**
* @test
*
* @param list<Comment> $originalComments
* @param list<Comment> $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());
}
}
13 changes: 13 additions & 0 deletions tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Sabberworm\CSS\Tests\Unit\Comment\Fixtures;

use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Comment\CommentContainer;

final class ConcreteCommentContainer implements Commentable
{
use CommentContainer;
}