Files
2026-10-17 16:39:28 +03:00

170 lines
5.9 KiB
PHP

<?php
use App\Models\Product;
use App\Models\Upvote;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
uses(RefreshDatabase::class);
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-10-17 15:00:00'));
});
afterEach(function () {
Carbon::setTestNow();
});
function lbProduct(string $title, string $state = 'approved', array $attrs = []): Product
{
return Product::factory()->{$state}()->create(['title' => $title] + $attrs);
}
function lbUpvotes(Product $product, int $count, ?Carbon $at = null): void
{
Upvote::factory()->count($count)->create([
'product_id' => $product->id,
'created_at' => $at ?? now(),
]);
}
test('top today ranks by upvotes received today, not by product age', function () {
$old = lbProduct('Old Favourite', attrs: ['created_at' => now()->subYear()]);
$new = lbProduct('Fresh Launch');
lbUpvotes($old, 3);
lbUpvotes($new, 1);
$this->get(route('leaderboard', ['period' => 'today']))
->assertOk()
->assertSeeInOrder(['Old Favourite', 'Fresh Launch']);
});
test('today only counts upvotes since midnight', function () {
$a = lbProduct('Upvoted Yesterday');
$b = lbProduct('Upvoted Today');
lbUpvotes($a, 5, now()->startOfDay()->subMinute());
lbUpvotes($b, 1, now()->startOfDay());
$this->get(route('leaderboard', ['period' => 'today']))
->assertSee('Upvoted Today')
->assertDontSee('Upvoted Yesterday');
});
test('week counts the last 7 days and month the last 30', function () {
$sixDays = lbProduct('Six Days Ago');
$eightDays = lbProduct('Eight Days Ago');
$twentyNine = lbProduct('Twenty Nine Days Ago');
$thirtyOne = lbProduct('Thirty One Days Ago');
lbUpvotes($sixDays, 1, now()->subDays(6));
lbUpvotes($eightDays, 1, now()->subDays(8));
lbUpvotes($twentyNine, 1, now()->subDays(29));
lbUpvotes($thirtyOne, 1, now()->subDays(31));
$this->get(route('leaderboard', ['period' => 'week']))
->assertSee('Six Days Ago')
->assertDontSee('Eight Days Ago')
->assertDontSee('Twenty Nine Days Ago');
$this->get(route('leaderboard', ['period' => 'month']))
->assertSee('Six Days Ago')
->assertSee('Eight Days Ago')
->assertSee('Twenty Nine Days Ago')
->assertDontSee('Thirty One Days Ago');
});
test('all time counts every upvote', function () {
$ancient = lbProduct('Ancient Product');
lbUpvotes($ancient, 1, now()->subYears(2));
$this->get(route('leaderboard', ['period' => 'all']))->assertSee('Ancient Product');
});
test('only approved products with upvotes in the period are ranked', function () {
$ok = lbProduct('Ranked Product');
$pending = lbProduct('Pending Product', 'pending');
$rejected = lbProduct('Rejected Product', 'rejected');
lbProduct('Zero Upvotes');
lbUpvotes($ok, 1);
lbUpvotes($pending, 4);
lbUpvotes($rejected, 4);
$this->get(route('leaderboard'))
->assertSee('Ranked Product')
->assertDontSee('Pending Product')
->assertDontSee('Rejected Product')
->assertDontSee('Zero Upvotes');
});
test('ties are broken by total upvotes, then by newest', function () {
$moreTotal = lbProduct('More Total', attrs: ['created_at' => now()->subDays(10)]);
$fewerTotal = lbProduct('Fewer Total', attrs: ['created_at' => now()->subDays(5)]);
$newerTie = lbProduct('Newer Tie', attrs: ['created_at' => now()->subDay()]);
$olderTie = lbProduct('Older Tie', attrs: ['created_at' => now()->subDays(2)]);
lbUpvotes($moreTotal, 2);
lbUpvotes($moreTotal, 3, now()->subDays(40));
lbUpvotes($fewerTotal, 2);
lbUpvotes($newerTie, 1);
lbUpvotes($olderTie, 1);
$this->get(route('leaderboard', ['period' => 'today']))
->assertSeeInOrder(['More Total', 'Fewer Total', 'Newer Tie', 'Older Tie']);
});
test('a removed upvote is not counted', function () {
$product = lbProduct('Changed Mind');
lbUpvotes($product, 2);
Upvote::where('product_id', $product->id)->first()->delete();
$this->get(route('leaderboard'))->assertSee('1 today', false);
});
test('new tab lists approved products newest first', function () {
lbProduct('Oldest One', attrs: ['created_at' => now()->subDays(3)]);
lbProduct('Newest One', attrs: ['created_at' => now()->subHour()]);
lbProduct('Middle One', attrs: ['created_at' => now()->subDay()]);
lbProduct('Pending One', 'pending');
$this->get(route('leaderboard', ['tab' => 'new']))
->assertOk()
->assertSeeInOrder(['Newest One', 'Middle One', 'Oldest One'])
->assertDontSee('Pending One');
});
test('lists 10 per page, ranks continue on page 2 and links keep the filters', function () {
foreach (range(1, 12) as $i) {
lbUpvotes(lbProduct("Product $i"), 20 - $i);
}
$page1 = $this->get(route('leaderboard', ['period' => 'week']));
$page1->assertSee('data-rank="10"', false)
->assertDontSee('data-rank="11"', false)
->assertSee('period=week', false)
->assertSee('page=2', false);
$this->get(route('leaderboard', ['period' => 'week', 'page' => 2]))
->assertSee('data-rank="11"', false)
->assertSee('data-rank="12"', false)
->assertSee('Product 12')
->assertDontSee('Product 1<', false);
});
test('invalid tab and period fall back to top today', function () {
$product = lbProduct('Fallback Product');
lbUpvotes($product, 1);
$this->get(route('leaderboard', ['tab' => 'nonsense', 'period' => 'decade']))
->assertOk()
->assertSee('Fallback Product');
});
test('empty period shows a message', function () {
$this->get(route('leaderboard', ['period' => 'today']))
->assertOk()
->assertSee('No upvotes yet today');
});
test('navbar links to the leaderboard', function () {
$this->get(route('leaderboard'))->assertSee(route('leaderboard'), false);
$this->get('/products')->assertSee(route('leaderboard'), false);
});