170 lines
5.9 KiB
PHP
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);
|
||
|
|
});
|