diff --git a/resources/views/comment-list.blade.php b/resources/views/comment-list.blade.php index 6ecbf31..76449b1 100644 --- a/resources/views/comment-list.blade.php +++ b/resources/views/comment-list.blade.php @@ -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) 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"; diff --git a/tests/Livewire/CommentListTest.php b/tests/Livewire/CommentListTest.php index 09f800b..e069a4a 100644 --- a/tests/Livewire/CommentListTest.php +++ b/tests/Livewire/CommentListTest.php @@ -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') @@ -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') @@ -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([ @@ -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(); @@ -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() @@ -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()); +});