feat: leaderboard with top (by period) and new tabs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
25e1dd2413
commit
3f8fa9f22f
@@ -0,0 +1,169 @@
|
||||
<?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);
|
||||
});
|
||||
Reference in New Issue
Block a user