withoutMiddleware([ VerifyCsrfToken::class, \Illuminate\Session\Middleware\AuthenticateSession::class, ]); } // ─── Shared edit modal: per-card copy is gone ───── public function test_video_card_does_not_render_a_per_card_edit_modal(): void { [$user, $video] = $this->makeUserAndVideo(); $this->actingAs($user); $html = view('components.video-card', ['video' => $video])->render(); // The dead per-card modal used id="editVideoModal{slug}" and the class // "cute-edit-modal". If either reappears, we're back to shipping 100 // extra lines per card. $this->assertStringNotContainsString('editVideoModal' . $video->getRouteKey(), $html); $this->assertStringNotContainsString('cute-edit-modal', $html); } // ─── Owner-only menu items ──────────────────────── public function test_owner_sees_edit_delete_items_in_card_menu(): void { [$owner, $video] = $this->makeUserAndVideo(); $this->actingAs($owner); $html = view('components.video-card', ['video' => $video])->render(); $this->assertStringContainsString('openEditVideoModal', $html); $this->assertStringContainsString('showDeleteModal', $html); $this->assertStringNotContainsString('notInterested', $html); $this->assertStringNotContainsString('dontRecommendChannel', $html); $this->assertStringNotContainsString('reportVideo', $html); } public function test_non_owner_sees_report_items_but_not_edit(): void { [$owner, $video] = $this->makeUserAndVideo(); $viewer = User::factory()->create([ 'username' => 'v'.substr(bin2hex(random_bytes(4)), 0, 8), ]); $this->actingAs($viewer); $html = view('components.video-card', ['video' => $video])->render(); $this->assertStringNotContainsString('openEditVideoModal', $html); $this->assertStringNotContainsString('showDeleteModal', $html); $this->assertStringContainsString('notInterested', $html); $this->assertStringContainsString('reportVideo', $html); } // ─── Thumbnail layout-shift attributes ──────────── public function test_thumbnail_image_has_width_and_height(): void { [$user, $video] = $this->makeUserAndVideo(); $this->actingAs($user); $html = view('components.video-card', ['video' => $video])->render(); $this->assertMatchesRegularExpression( '/]*width="320"[^>]*height="180"/i', $html, 'Thumbnail img must carry explicit width/height so the browser reserves layout space.' ); $this->assertStringContainsString('loading="lazy"', $html); } // ─── Cursor-paginated feed endpoint ─────────────── public function test_feed_more_returns_json_with_cards_and_next_cursor(): void { $user = User::factory()->create([ 'username' => 'f'.substr(bin2hex(random_bytes(4)), 0, 8), ]); // Twice the page size so the cursor must advance. for ($i = 0; $i < 30; $i++) { Video::create([ 'user_id' => $user->id, 'title' => 'Vid ' . $i, 'description' => '', 'filename' => "users/x/videos/y{$i}/video.mp4", 'path' => "users/x/videos/y{$i}/video.mp4", 'type' => 'generic', 'visibility' => 'public', 'status' => 'ready', 'share_token' => bin2hex(random_bytes(8)), ]); } $r = $this->getJson(route('videos.feed.more', ['filter' => 'all'])); $r->assertOk(); $r->assertJsonStructure(['items', 'next_cursor', 'has_more']); $this->assertTrue($r->json('has_more'), 'With 30 videos and a 24-page size, has_more must be true.'); $this->assertNotEmpty($r->json('next_cursor')); $this->assertStringContainsString('yt-video-card', (string) $r->json('items')); } public function test_feed_more_unknown_filter_returns_empty_slice(): void { $this->getJson(route('videos.feed.more', ['filter' => 'nonsense'])) ->assertOk() ->assertJson(['items' => '', 'has_more' => false]); } // ─── Lazy reply-form template present ───────────── public function test_comments_component_ships_the_reply_template_not_inline_forms(): void { [$user, $video] = $this->makeUserAndVideo(); \App\Models\Comment::create([ 'user_id' => $user->id, 'video_id' => $video->id, 'body' => 'top-level comment', ]); $this->actingAs($user); $html = view('components.video-comments', ['video' => $video])->render(); $this->assertStringContainsString('id="ytc-reply-form-tpl"', $html); $this->assertStringContainsString('ytc-reply-slot', $html); } // ─── Helpers ────────────────────────────────────── private function makeUserAndVideo(): array { $user = User::factory()->create([ 'username' => 'u'.substr(bin2hex(random_bytes(4)), 0, 8), ]); $video = Video::create([ 'user_id' => $user->id, 'title' => 'Test video', 'description'=> 'Hello', 'filename' => 'users/x/videos/y/video.mp4', 'path' => 'users/x/videos/y/video.mp4', 'type' => 'generic', 'visibility' => 'public', 'status' => 'ready', 'share_token' => bin2hex(random_bytes(8)), ]); return [$user, $video]; } }