search and filtering

This commit is contained in:
Doar Mihai
2026-02-07 14:36:33 +02:00
parent 7a99c3eb59
commit 89be4d4329
10 changed files with 1506 additions and 23 deletions
@@ -0,0 +1,390 @@
<?php
namespace Tests\Feature;
use App\Models\Category;
use App\Models\Product;
use App\Models\User;
use App\Models\Upvote;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProductSearchAndFilterTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Category $category1;
protected Category $category2;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
$this->category1 = Category::factory()->create(['name' => 'Technology']);
$this->category2 = Category::factory()->create(['name' => 'Design']);
}
/** @test */
public function it_can_search_products_by_title()
{
$product1 = Product::factory()->create([
'title' => 'Laravel Framework',
'description' => 'A web application framework',
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$product2 = Product::factory()->create([
'title' => 'React Library',
'description' => 'A JavaScript library',
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$response = $this->get(route('products.index', ['search' => 'Laravel']));
$response->assertStatus(200);
$response->assertSee($product1->title);
$response->assertDontSee($product2->title);
}
/** @test */
public function it_can_search_products_by_description()
{
$product1 = Product::factory()->create([
'title' => 'Product One',
'description' => 'This is about Laravel framework',
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$product2 = Product::factory()->create([
'title' => 'Product Two',
'description' => 'This is about React library',
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$response = $this->get(route('products.index', ['search' => 'Laravel']));
$response->assertStatus(200);
$response->assertSee($product1->title);
$response->assertDontSee($product2->title);
}
/** @test */
public function it_can_filter_products_by_category()
{
$techProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$designProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category2->id,
]);
$response = $this->get(route('products.index', ['category' => $this->category1->id]));
$response->assertStatus(200);
$response->assertSee($techProduct->title);
$response->assertDontSee($designProduct->title);
}
/** @test */
public function it_can_filter_products_by_date_range_week()
{
$oldProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subWeeks(2),
]);
$recentProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subDays(3),
]);
$response = $this->get(route('products.index', ['date_range' => 'week']));
$response->assertStatus(200);
$response->assertSee($recentProduct->title);
$response->assertDontSee($oldProduct->title);
}
/** @test */
public function it_can_filter_products_by_date_range_month()
{
$oldProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subMonths(2),
]);
$recentProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subDays(15),
]);
$response = $this->get(route('products.index', ['date_range' => 'month']));
$response->assertStatus(200);
$response->assertSee($recentProduct->title);
$response->assertDontSee($oldProduct->title);
}
/** @test */
public function it_can_filter_products_by_date_range_today()
{
$yesterdayProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subDay(),
]);
$todayProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now(),
]);
$response = $this->get(route('products.index', ['date_range' => 'today']));
$response->assertStatus(200);
$response->assertSee($todayProduct->title);
$response->assertDontSee($yesterdayProduct->title);
}
/** @test */
public function it_can_sort_products_by_popularity()
{
$product1 = Product::factory()->create([
'title' => 'Less Popular Product',
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$product2 = Product::factory()->create([
'title' => 'More Popular Product',
'status' => 'approved',
'category_id' => $this->category1->id,
]);
// Add upvotes to product2
Upvote::factory()->count(5)->create(['product_id' => $product2->id]);
Upvote::factory()->count(2)->create(['product_id' => $product1->id]);
$response = $this->get(route('products.index', ['sort' => 'popular']));
$response->assertStatus(200);
// Get the response content and check if product2 appears before product1
$content = $response->getContent();
$pos1 = strpos($content, $product2->title);
$pos2 = strpos($content, $product1->title);
$this->assertNotFalse($pos1);
$this->assertNotFalse($pos2);
$this->assertLessThan($pos2, $pos1, 'More popular product should appear first');
}
/** @test */
public function it_can_sort_products_by_newest()
{
$oldProduct = Product::factory()->create([
'title' => 'Old Product',
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subDays(5),
]);
$newProduct = Product::factory()->create([
'title' => 'New Product',
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now(),
]);
$response = $this->get(route('products.index', ['sort' => 'newest']));
$response->assertStatus(200);
// Get the response content and check if newProduct appears before oldProduct
$content = $response->getContent();
$pos1 = strpos($content, $newProduct->title);
$pos2 = strpos($content, $oldProduct->title);
$this->assertNotFalse($pos1);
$this->assertNotFalse($pos2);
$this->assertLessThan($pos2, $pos1, 'Newer product should appear first');
}
/** @test */
public function it_can_sort_products_by_oldest()
{
$oldProduct = Product::factory()->create([
'title' => 'Old Product',
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subDays(5),
]);
$newProduct = Product::factory()->create([
'title' => 'New Product',
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now(),
]);
$response = $this->get(route('products.index', ['sort' => 'oldest']));
$response->assertStatus(200);
// Get the response content and check if oldProduct appears before newProduct
$content = $response->getContent();
$pos1 = strpos($content, $oldProduct->title);
$pos2 = strpos($content, $newProduct->title);
$this->assertNotFalse($pos1);
$this->assertNotFalse($pos2);
$this->assertLessThan($pos2, $pos1, 'Older product should appear first');
}
/** @test */
public function it_can_combine_search_and_filters()
{
$matchingProduct = Product::factory()->create([
'title' => 'Laravel Tool',
'description' => 'A great tool',
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subDays(2),
]);
$nonMatchingProduct1 = Product::factory()->create([
'title' => 'Laravel Design',
'description' => 'A design tool',
'status' => 'approved',
'category_id' => $this->category2->id, // Different category
'created_at' => now()->subDays(2),
]);
$nonMatchingProduct2 = Product::factory()->create([
'title' => 'React Tool',
'description' => 'A different tool',
'status' => 'approved',
'category_id' => $this->category1->id,
'created_at' => now()->subDays(2),
]);
$response = $this->get(route('products.index', [
'search' => 'Laravel',
'category' => $this->category1->id,
'date_range' => 'week',
]));
$response->assertStatus(200);
$response->assertSee($matchingProduct->title);
$response->assertDontSee($nonMatchingProduct1->title);
$response->assertDontSee($nonMatchingProduct2->title);
}
/** @test */
public function it_only_shows_approved_products_on_public_index()
{
$approvedProduct = Product::factory()->create([
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$pendingProduct = Product::factory()->create([
'status' => 'pending',
'category_id' => $this->category1->id,
]);
$rejectedProduct = Product::factory()->create([
'status' => 'rejected',
'category_id' => $this->category1->id,
]);
$response = $this->get(route('products.index'));
$response->assertStatus(200);
$response->assertSee($approvedProduct->title);
$response->assertDontSee($pendingProduct->title);
$response->assertDontSee($rejectedProduct->title);
}
/** @test */
public function it_preserves_query_parameters_in_pagination()
{
// Create enough products to trigger pagination
Product::factory()->count(15)->create([
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$response = $this->get(route('products.index', [
'search' => 'test',
'category' => $this->category1->id,
'sort' => 'popular',
]));
$response->assertStatus(200);
// Check that pagination links include the query parameters
$response->assertSee('search=test');
$response->assertSee('category=' . $this->category1->id);
$response->assertSee('sort=popular');
}
/** @test */
public function it_validates_search_and_filter_parameters()
{
$response = $this->get(route('products.index', [
'category' => 99999, // Non-existent category
]));
$response->assertSessionHasErrors('category');
}
/** @test */
public function it_validates_invalid_sort_parameter()
{
$response = $this->get(route('products.index', [
'sort' => 'invalid_sort',
]));
$response->assertSessionHasErrors('sort');
}
/** @test */
public function it_validates_invalid_date_range_parameter()
{
$response = $this->get(route('products.index', [
'date_range' => 'invalid_range',
]));
$response->assertSessionHasErrors('date_range');
}
/** @test */
public function it_shows_appropriate_message_when_no_results_found()
{
Product::factory()->create([
'title' => 'Laravel Tool',
'status' => 'approved',
'category_id' => $this->category1->id,
]);
$response = $this->get(route('products.index', ['search' => 'nonexistentproduct']));
$response->assertStatus(200);
$response->assertSee('No products found matching your filters');
}
}