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); });