feat: tag-based product alternatives
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fbb4862454
commit
c4e6173e46
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Upvote;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function altTags(int $n): array
|
||||
{
|
||||
return collect(range(1, $n))->map(fn ($i) => Tag::create([
|
||||
'name' => "Alt Tag $i",
|
||||
'slug' => "alt-tag-$i",
|
||||
]))->all();
|
||||
}
|
||||
|
||||
function altProduct(array $tags, string $state = 'approved', array $attrs = []): Product
|
||||
{
|
||||
$product = Product::factory()->{$state}()->create($attrs);
|
||||
$product->tags()->sync(collect($tags)->pluck('id')->all());
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
test('orders by shared tags', function () {
|
||||
[$a, $b, $c, $d] = altTags(4);
|
||||
$self = altProduct([$a, $b, $c]);
|
||||
$two = altProduct([$a, $b]);
|
||||
$three = altProduct([$a, $b, $c, $d]);
|
||||
$one = altProduct([$a]);
|
||||
|
||||
$result = $self->alternatives();
|
||||
|
||||
expect($result->pluck('id')->all())->toBe([$three->id, $two->id, $one->id]);
|
||||
expect($result->pluck('shared_tags_count')->map(fn ($v) => (int) $v)->all())->toBe([3, 2, 1]);
|
||||
});
|
||||
|
||||
test('excludes self, pending, rejected and zero-shared-tag products', function () {
|
||||
[$a, $b] = altTags(2);
|
||||
$self = altProduct([$a]);
|
||||
$good = altProduct([$a]);
|
||||
altProduct([$a], 'pending');
|
||||
altProduct([$a], 'rejected');
|
||||
altProduct([$b]);
|
||||
|
||||
expect($self->alternatives()->pluck('id')->all())->toBe([$good->id]);
|
||||
});
|
||||
|
||||
test('ties are ordered by upvotes then newest', function () {
|
||||
[$a] = altTags(1);
|
||||
$self = altProduct([$a]);
|
||||
$old = altProduct([$a], 'approved', ['created_at' => now()->subDays(3)]);
|
||||
$new = altProduct([$a], 'approved', ['created_at' => now()->subDay()]);
|
||||
$voted = altProduct([$a], 'approved', ['created_at' => now()->subDays(5)]);
|
||||
Upvote::create(['product_id' => $voted->id, 'user_id' => userWithRole('user')->id]);
|
||||
|
||||
expect($self->alternatives()->pluck('id')->all())->toBe([$voted->id, $new->id, $old->id]);
|
||||
});
|
||||
|
||||
test('limits to 3', function () {
|
||||
[$a] = altTags(1);
|
||||
$self = altProduct([$a]);
|
||||
foreach (range(1, 5) as $i) {
|
||||
altProduct([$a]);
|
||||
}
|
||||
|
||||
expect($self->alternatives())->toHaveCount(3);
|
||||
expect($self->alternatives(2))->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('alternatives is a single query', function () {
|
||||
[$a, $b] = altTags(2);
|
||||
$self = altProduct([$a, $b]);
|
||||
altProduct([$a]);
|
||||
altProduct([$b]);
|
||||
|
||||
DB::enableQueryLog();
|
||||
$self->alternatives();
|
||||
expect(DB::getQueryLog())->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('show page renders alternative cards and hides the section when none', function () {
|
||||
[$a] = altTags(1);
|
||||
$self = altProduct([$a]);
|
||||
|
||||
$this->get(route('products.show', $self))->assertOk()->assertDontSee('data-alternatives', false);
|
||||
|
||||
$alt = altProduct([$a], 'approved', ['title' => 'Cool Alternative', 'eu_made' => true]);
|
||||
|
||||
$this->get(route('products.show', $self))
|
||||
->assertOk()
|
||||
->assertSee('data-alternatives', false)
|
||||
->assertSee('Cool Alternative')
|
||||
->assertSee(route('products.show', $alt), false)
|
||||
->assertSee('1 shared tag')
|
||||
->assertSee('EU made');
|
||||
});
|
||||
Reference in New Issue
Block a user