Files
SkinbaseNova/tests/Feature/Artworks/M18B1CommentReactionReadTest.php
klevze bf902f509a Batch-load comment reaction counts on artwork comments.
Avoid per-comment reaction queries on the deferred comments API by aggregating counts in one service call.
2026-08-29 12:25:37 +02:00

147 lines
5.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Enums\ReactionType;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use App\Models\CommentReaction;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
uses(RefreshDatabase::class);
function m18b1ArtworkCommentTree(int $topLevelCount = 2): array
{
$artwork = Artwork::factory()->create();
$author = User::factory()->create();
$viewer = User::factory()->create();
$other = User::factory()->create();
$comments = [];
for ($i = 0; $i < $topLevelCount; $i++) {
$comment = ArtworkComment::factory()->create([
'artwork_id' => $artwork->id,
'user_id' => $author->id,
'parent_id' => null,
]);
$reply = ArtworkComment::factory()->create([
'artwork_id' => $artwork->id,
'user_id' => $other->id,
'parent_id' => $comment->id,
]);
CommentReaction::create([
'comment_id' => $comment->id,
'user_id' => $other->id,
'reaction' => ReactionType::Heart->value,
]);
CommentReaction::create([
'comment_id' => $reply->id,
'user_id' => $viewer->id,
'reaction' => ReactionType::Fire->value,
]);
CommentReaction::create([
'comment_id' => $reply->id,
'user_id' => $other->id,
'reaction' => ReactionType::Clap->value,
]);
$comments[] = [$comment, $reply];
}
return [$artwork, $viewer, $comments];
}
function m18b1ReactionQueryCount(array $queries): int
{
return collect($queries)
->filter(fn (array $query): bool => str_contains(strtolower((string) $query['query']), 'comment_reactions'))
->count();
}
it('includes bulk reaction totals for guests with one reaction query', function (): void {
[$artwork, , $comments] = m18b1ArtworkCommentTree();
DB::enableQueryLog();
DB::flushQueryLog();
$response = $this->getJson("/api/artworks/{$artwork->id}/comments")->assertOk();
$payload = $response->json('data');
$withoutReactions = json_decode(json_encode($payload, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
$stripReactions = function (array &$items) use (&$stripReactions): void {
foreach ($items as &$item) {
unset($item['reactions']);
if (isset($item['replies']) && is_array($item['replies'])) {
$stripReactions($item['replies']);
}
}
};
$stripReactions($withoutReactions);
$commentCount = 0;
$countComments = function (array $items) use (&$countComments, &$commentCount): void {
foreach ($items as $item) {
$commentCount++;
if (isset($item['replies']) && is_array($item['replies'])) {
$countComments($item['replies']);
}
}
};
$countComments($payload);
fwrite(STDERR, sprintf(
"M18B1 guest payload bytes: before=%d after=%d added=%d comments/replies=%d\n",
strlen(json_encode($withoutReactions, JSON_THROW_ON_ERROR)),
strlen(json_encode($payload, JSON_THROW_ON_ERROR)),
strlen(json_encode($payload, JSON_THROW_ON_ERROR)) - strlen(json_encode($withoutReactions, JSON_THROW_ON_ERROR)),
$commentCount,
));
expect(m18b1ReactionQueryCount(DB::getQueryLog()))->toBe(1)
->and($payload[0])->toHaveKey('reactions')
->and($payload[0]['reactions'][ReactionType::Heart->value]['count'])->toBe(1)
->and($payload[0]['reactions'][ReactionType::Heart->value]['mine'])->toBeFalse()
->and($payload[0]['replies'][0]['reactions'][ReactionType::Fire->value]['count'])->toBe(1)
->and(count($comments))->toBe(2);
DB::disableQueryLog();
});
it('includes viewer reaction state for comments and replies with one extra query', function (): void {
[$artwork, $viewer] = m18b1ArtworkCommentTree();
DB::enableQueryLog();
DB::flushQueryLog();
$response = $this->actingAs($viewer)
->getJson("/api/artworks/{$artwork->id}/comments")
->assertOk();
$payload = $response->json('data');
expect(m18b1ReactionQueryCount(DB::getQueryLog()))->toBe(2)
->and($payload[0]['reactions'][ReactionType::Heart->value]['mine'])->toBeFalse()
->and($payload[0]['replies'][0]['reactions'][ReactionType::Fire->value]['mine'])->toBeTrue();
DB::disableQueryLog();
});
it('keeps reaction query count constant as the comment tree grows', function (): void {
[$smallArtwork] = m18b1ArtworkCommentTree(2);
[$largeArtwork] = m18b1ArtworkCommentTree(20);
DB::enableQueryLog();
DB::flushQueryLog();
$this->getJson("/api/artworks/{$smallArtwork->id}/comments")->assertOk();
$smallCount = m18b1ReactionQueryCount(DB::getQueryLog());
DB::flushQueryLog();
$this->getJson("/api/artworks/{$largeArtwork->id}/comments")->assertOk();
$largeCount = m18b1ReactionQueryCount(DB::getQueryLog());
DB::disableQueryLog();
expect($smallCount)->toBe(1)->and($largeCount)->toBe(1);
});