create(['name' => 'Seo Author']); $contentType = ContentType::create([ 'name' => 'Skins', 'slug' => 'skins', 'description' => 'Skins content type', ]); $category = Category::create([ 'content_type_id' => $contentType->id, 'name' => 'Classic', 'slug' => 'classic', 'description' => 'Classic skins', 'is_active' => true, 'sort_order' => 1, ]); for ($i = 1; $i <= 25; $i++) { $artwork = Artwork::factory() ->for($user) ->create([ 'title' => 'Seo Item ' . $i, 'slug' => 'seo-item-' . $i, 'published_at' => now()->subMinutes($i), ]); $artwork->categories()->attach($category->id); } $response = $this->get('/browse?limit=12&grid=v2'); $response->assertOk(); $html = $response->getContent(); $this->assertNotFalse($html); $this->assertStringContainsString('', $html); $this->assertMatchesRegularExpression('//i', $html); preg_match('//i', $html, $canonicalMatches); $this->assertArrayHasKey(1, $canonicalMatches); $canonicalUrl = html_entity_decode((string) $canonicalMatches[1], ENT_QUOTES); $this->assertStringNotContainsString('grid=v2', $canonicalUrl); $this->assertMatchesRegularExpression('//i', $html); preg_match('//i', $html, $nextMatches); $this->assertArrayHasKey(1, $nextMatches); $nextUrl = html_entity_decode((string) $nextMatches[1], ENT_QUOTES); $this->assertStringContainsString('cursor=', $nextUrl); $this->assertStringNotContainsString('grid=v2', $nextUrl); $secondPage = $this->get($nextUrl); $secondPage->assertOk(); $secondHtml = $secondPage->getContent(); $this->assertNotFalse($secondHtml); $this->assertMatchesRegularExpression('//i', $secondHtml); preg_match('//i', $secondHtml, $prevMatches); $this->assertArrayHasKey(1, $prevMatches); $prevUrl = html_entity_decode((string) $prevMatches[1], ENT_QUOTES); $this->assertStringNotContainsString('grid=v2', $prevUrl); $this->assertMatchesRegularExpression('//i', $secondHtml, $secondCanonicalMatches); $this->assertArrayHasKey(1, $secondCanonicalMatches); $secondCanonicalUrl = html_entity_decode((string) $secondCanonicalMatches[1], ENT_QUOTES); $this->assertStringNotContainsString('grid=v2', $secondCanonicalUrl); $pageOne = $this->get('/browse?limit=12&page=1&grid=v2'); $pageOne->assertOk(); $pageOneHtml = $pageOne->getContent(); $this->assertNotFalse($pageOneHtml); $this->assertMatchesRegularExpression('//i', $pageOneHtml); preg_match('//i', $pageOneHtml, $pageOneCanonicalMatches); $this->assertArrayHasKey(1, $pageOneCanonicalMatches); $pageOneCanonicalUrl = html_entity_decode((string) $pageOneCanonicalMatches[1], ENT_QUOTES); $this->assertStringNotContainsString('page=1', $pageOneCanonicalUrl); } public function test_api_browse_supports_limit_and_cursor_pagination(): void { $user = User::factory()->create(['name' => 'Cursor Author']); $contentType = ContentType::create([ 'name' => 'Skins', 'slug' => 'skins', 'description' => 'Skins content type', ]); $category = Category::create([ 'content_type_id' => $contentType->id, 'name' => 'Winamp', 'slug' => 'winamp', 'description' => 'Winamp skins', 'is_active' => true, 'sort_order' => 1, ]); for ($i = 1; $i <= 6; $i++) { $artwork = Artwork::factory() ->for($user) ->create([ 'title' => 'Cursor Item ' . $i, 'slug' => 'cursor-item-' . $i, 'published_at' => now()->subMinutes($i), ]); $artwork->categories()->attach($category->id); } $first = $this->getJson('/api/v1/browse?limit=2'); $first->assertOk(); $first->assertJsonCount(2, 'data'); $nextCursor = (string) data_get($first->json(), 'links.next', ''); $this->assertNotEmpty($nextCursor); $this->assertStringContainsString('cursor=', $nextCursor); $second = $this->getJson($nextCursor); $second->assertOk(); $second->assertJsonCount(2, 'data'); $firstFirstSlug = data_get($first->json(), 'data.0.slug'); $secondFirstSlug = data_get($second->json(), 'data.0.slug'); $this->assertNotSame($firstFirstSlug, $secondFirstSlug); } public function test_api_browse_returns_public_artworks(): void { $user = User::factory()->create(['name' => 'Author One']); $contentType = ContentType::create([ 'name' => 'Wallpapers', 'slug' => 'wallpapers', 'description' => 'Wallpapers content type', ]); $category = Category::create([ 'content_type_id' => $contentType->id, 'name' => 'Abstract', 'slug' => 'abstract', 'description' => 'Abstract wallpapers', 'is_active' => true, 'sort_order' => 1, ]); $artwork = Artwork::factory() ->for($user) ->create([ 'slug' => 'neon-city', 'published_at' => now()->subDay(), ]); $artwork->categories()->attach($category->id); $response = $this->getJson('/api/v1/browse'); $response->assertOk() ->assertJsonPath('data.0.slug', 'neon-city') ->assertJsonPath('data.0.category.slug', 'abstract') ->assertJsonPath('data.0.author.name', 'Author One'); } public function test_web_browse_shows_artworks(): void { $user = User::factory()->create(['name' => 'Author Two']); $contentType = ContentType::create([ 'name' => 'Photography', 'slug' => 'photography', 'description' => 'Photos', ]); $category = Category::create([ 'content_type_id' => $contentType->id, 'name' => 'Nature', 'slug' => 'nature', 'description' => 'Nature photos', 'is_active' => true, 'sort_order' => 1, ]); $artwork = Artwork::factory() ->for($user) ->create([ 'title' => 'Forest Light', 'slug' => 'forest-light', 'published_at' => now()->subDay(), ]); $artwork->categories()->attach($category->id); $response = $this->get('/browse'); $response->assertOk(); $response->assertSee('Forest Light'); $response->assertSee('Author Two'); $html = $response->getContent(); $this->assertNotFalse($html); $this->assertStringContainsString('itemprop="thumbnailUrl"', $html); // First card (index 0) is eager-loaded with fetchpriority=high — no blur-preview $this->assertStringContainsString('loading="eager"', $html); $this->assertStringContainsString('decoding="sync"', $html); $this->assertStringContainsString('fetchpriority="high"', $html); $this->assertMatchesRegularExpression('/]*loading="eager"[^>]*width="\d+"[^>]*height="\d+"/i', $html); } }