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