Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion resources/views/comment-list.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class="comm:w-8 comm:h-8 comm:text-gray-400 comm:dark:text-gray-500"
@foreach ($this->comments as $comment)
<livewire:dynamic-component
:component="$commentionsComponentPrefix . 'comment'"
:key="$comment->getContentHash()"
:key="$comment::class . ':' . $comment->getId()"
:comment="$comment"
:mentionables="$mentionables"
:tip-tap-css-classes="$tipTapCssClasses"
Expand Down
3 changes: 3 additions & 0 deletions src/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ public function getLabel(): ?string
return null;
}

/**
* @deprecated No longer used internally for Livewire keys; will be removed in the next major version.
*/
public function getContentHash(): string
{
return md5(json_encode([
Expand Down
3 changes: 3 additions & 0 deletions src/Contracts/RenderableComment.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@ public function getUpdatedAt(): \DateTime|CarbonInterface;

public function getLabel(): ?string;

/**
* @deprecated No longer used internally for Livewire keys; will be removed in the next major version.
*/
public function getContentHash(): string;
}
3 changes: 3 additions & 0 deletions src/RenderableComment.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public function getLabel(): ?string
return $this->label;
}

/**
* @deprecated No longer used internally for Livewire keys; will be removed in the next major version.
*/
public function getContentHash(): string
{
return "comment-$this->id";
Expand Down
102 changes: 97 additions & 5 deletions tests/Livewire/CommentListTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@
use Kirschbaum\Commentions\Comment as CommentModel;
use Kirschbaum\Commentions\Livewire\CommentList;
use Kirschbaum\Commentions\RenderableComment;
use Mockery;
use Mockery\MockInterface;
use Tests\Models\Post;
use Tests\Models\User;

use function Pest\Livewire\livewire;

function assertCommentKey($component, string $class, int|string $id): void
{
$html = $component->html();

$literal = 'wire:key="' . $class . ':' . $id . '"';
$snapshot = trim(json_encode("$class:$id"), '"');

expect(str_contains($html, $literal) || str_contains($html, $snapshot))->toBeTrue();
}

test('CommentList calls getComments when not paginating', function () {
/** @var Post|Mockery\MockInterface $post */
/** @var Post|MockInterface $post */
$post = Mockery::mock(Post::class)->makePartial();

$post->shouldReceive('getComments')
Expand All @@ -26,7 +36,7 @@
});

test('CommentList calls getComments when paginating', function () {
/** @var Post|Mockery\MockInterface $post */
/** @var Post|MockInterface $post */
$post = Mockery::mock(Post::class)->makePartial();

$post->shouldReceive('getComments')
Expand All @@ -43,7 +53,7 @@
});

test('CommentList can render non-Comment renderable items', function () {
/** @var Post|Mockery\MockInterface $post */
/** @var Post|MockInterface $post */
$post = Mockery::mock(Post::class)->makePartial();

$items = collect([
Expand All @@ -65,6 +75,56 @@
->assertSee('Automated message');
});

test('CommentList renders duplicate-content comments without key collision', function () {
/** @var User $user */
$user = User::factory()->create();
/** @var Post $realPost */
$realPost = Post::factory()->create();

$first = CommentModel::factory()->author($user)->commentable($realPost)->create([
'body' => 'identical body',
]);
$second = CommentModel::factory()->author($user)->commentable($realPost)->create([
'body' => 'identical body',
]);

$component = livewire(CommentList::class, [
'record' => $realPost,
'paginate' => false,
]);

assertCommentKey($component, CommentModel::class, $first->id);
assertCommentKey($component, CommentModel::class, $second->id);
});

test('CommentList keeps comment keys stable across edits', function () {
/** @var User $user */
$user = User::factory()->create();
/** @var Post $realPost */
$realPost = Post::factory()->create();

/** @var CommentModel $comment */
$comment = CommentModel::factory()->author($user)->commentable($realPost)->create([
'body' => 'original body',
]);

$before = livewire(CommentList::class, [
'record' => $realPost,
'paginate' => false,
]);

assertCommentKey($before, CommentModel::class, $comment->id);

$comment->update(['body' => 'edited body']);

$after = livewire(CommentList::class, [
'record' => $realPost,
'paginate' => false,
]);

assertCommentKey($after, CommentModel::class, $comment->id);
});

test('CommentList can render both Comment and RenderableComment items', function () {
/** @var User $user */
$user = User::factory()->create();
Expand All @@ -83,7 +143,7 @@

$items = collect([$comment, $renderable]);

/** @var Post|Mockery\MockInterface $post */
/** @var Post|MockInterface $post */
$post = Mockery::mock(Post::class)->makePartial();
$post->shouldReceive('getComments')
->once()
Expand All @@ -100,3 +160,35 @@
->assertSee('System')
->assertSee('System message');
});

test('CommentList renders Comment and RenderableComment sharing an id without key collision', function () {
/** @var User $user */
$user = User::factory()->create();
/** @var Post $realPost */
$realPost = Post::factory()->create();

/** @var CommentModel $comment */
$comment = CommentModel::factory()
->author($user)
->commentable($realPost)
->create([
'body' => 'Real comment body',
]);

// RenderableComment deliberately reuses the Comment's primary key.
$renderable = new RenderableComment(id: $comment->id, authorName: 'System', body: 'System message');

/** @var Post|MockInterface $post */
$post = Mockery::mock(Post::class)->makePartial();
$post->shouldReceive('getComments')
->once()
->andReturn(collect([$comment, $renderable]));

$component = livewire(CommentList::class, [
'record' => $post,
'paginate' => false,
]);

assertCommentKey($component, CommentModel::class, $comment->id);
assertCommentKey($component, RenderableComment::class, $renderable->getId());
});
Loading