diff --git a/resources/views/videos/types/generic.blade.php b/resources/views/videos/types/generic.blade.php index 36e4206..f484770 100644 --- a/resources/views/videos/types/generic.blade.php +++ b/resources/views/videos/types/generic.blade.php @@ -980,9 +980,13 @@ @if ($recommendedVideos && $recommendedVideos->count() > 0) - + @endforeach @else diff --git a/resources/views/videos/types/match.blade.php b/resources/views/videos/types/match.blade.php index b1cd764..901a854 100644 --- a/resources/views/videos/types/match.blade.php +++ b/resources/views/videos/types/match.blade.php @@ -3144,9 +3144,13 @@ @if ($recommendedVideos && $recommendedVideos->count() > 0) - + @endforeach @else diff --git a/resources/views/videos/types/music.blade.php b/resources/views/videos/types/music.blade.php index 120380f..9c7f6af 100644 --- a/resources/views/videos/types/music.blade.php +++ b/resources/views/videos/types/music.blade.php @@ -972,9 +972,13 @@ @if ($recommendedVideos && $recommendedVideos->count() > 0) - + @endforeach @else diff --git a/tests/Feature/Phase4WatchTest.php b/tests/Feature/Phase4WatchTest.php new file mode 100644 index 0000000..13b0224 --- /dev/null +++ b/tests/Feature/Phase4WatchTest.php @@ -0,0 +1,180 @@ +withoutMiddleware([ + VerifyCsrfToken::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, + ]); + } + + // ─── Comment component ──────────────────────────── + + public function test_comment_header_uses_singular_count_for_one_comment(): void + { + [$user, $video] = $this->makeUserAndVideo(); + Comment::create([ + 'user_id' => $user->id, + 'video_id' => $video->id, + 'body' => 'the only comment', + ]); + $this->actingAs($user); + + $html = view('components.video-comments', ['video' => $video])->render(); + + $this->assertStringContainsString('1 Comment', $html); + $this->assertStringNotContainsString('1 Comments', $html); + } + + public function test_comment_header_breaks_out_replies_from_top_level(): void + { + [$user, $video] = $this->makeUserAndVideo(); + $parent = Comment::create([ + 'user_id' => $user->id, + 'video_id' => $video->id, + 'body' => 'top', + ]); + Comment::create([ + 'user_id' => $user->id, + 'video_id' => $video->id, + 'parent_id' => $parent->id, + 'body' => 'reply 1', + ]); + Comment::create([ + 'user_id' => $user->id, + 'video_id' => $video->id, + 'parent_id' => $parent->id, + 'body' => 'reply 2', + ]); + $this->actingAs($user); + + $html = view('components.video-comments', ['video' => $video])->render(); + + $this->assertStringContainsString('1 Comment', $html, 'Top-level count is 1.'); + $this->assertStringContainsString('2 replies', $html, 'Replies are counted separately.'); + } + + public function test_each_comment_timestamp_is_a_lc_permalink(): void + { + [$user, $video] = $this->makeUserAndVideo(); + $c = Comment::create([ + 'user_id' => $user->id, + 'video_id' => $video->id, + 'body' => 'hi', + ]); + $this->actingAs($user); + + $html = view('components.video-comments', ['video' => $video])->render(); + + $this->assertStringContainsString('?lc=' . $c->id . '#comment-' . $c->id, $html); + $this->assertStringContainsString('ytc-permalink', $html); + } + + // ─── Description markdown ──────────────────────── + + public function test_leaked_markdown_headers_render_as_html(): void + { + $out = HtmlSanitizer::render("Intro line\n## Letra\nla la\n### Chorus"); + + $this->assertStringContainsString('

Letra

', $out); + $this->assertStringContainsString('

Chorus

', $out); + $this->assertStringNotContainsString('## Letra', $out); + $this->assertStringNotContainsString('### Chorus', $out); + } + + public function test_normal_plain_text_description_passes_through(): void + { + $out = HtmlSanitizer::render("hello world\nhow are you"); + + $this->assertStringContainsString('hello world', $out); + $this->assertStringContainsString('how are you', $out); + $this->assertStringNotContainsString('

', $out); + } + + // ─── Download access gate ──────────────────────── + + public function test_disabled_download_returns_403_for_mp4(): void + { + [$user, $video] = $this->makeUserAndVideo(['download_access' => 'disabled']); + + $this->get(route('videos.download', $video))->assertStatus(403); + } + + public function test_disabled_download_returns_403_for_mp3(): void + { + [$user, $video] = $this->makeUserAndVideo(['download_access' => 'disabled']); + + $this->get(route('videos.downloadMp3', $video))->assertStatus(403); + } + + public function test_registered_download_denies_guests(): void + { + [$user, $video] = $this->makeUserAndVideo(['download_access' => 'registered']); + + $this->get(route('videos.download', $video))->assertStatus(403); + } + + // ─── Up Next anchor cards ───────────────────────── + + public function test_up_next_sidebar_renders_anchor_cards_with_hrefs(): void + { + [$owner, $target] = $this->makeUserAndVideo(); + [, $rec] = $this->makeUserAndVideo(); + + $html = $this->get(route('videos.show', $target))->getContent(); + + $this->assertStringContainsString('assertStringContainsString('data-rec-url=', $html, + 'data-rec-url hooks keep the SPA transition working for left-clicks.'); + } + + // ─── Helpers ────────────────────────────────────── + + private function makeUserAndVideo(array $attrs = []): array + { + $user = User::factory()->create([ + 'username' => 'u'.substr(bin2hex(random_bytes(4)), 0, 8), + ]); + $video = Video::create(array_merge([ + 'user_id' => $user->id, + 'title' => 'T '.substr(bin2hex(random_bytes(4)), 0, 6), + 'description'=> '', + 'filename' => 'users/x/videos/'.substr(bin2hex(random_bytes(4)), 0, 8).'/video.mp4', + 'path' => 'users/x/videos/'.substr(bin2hex(random_bytes(4)), 0, 8).'/video.mp4', + 'type' => 'generic', + 'visibility' => 'public', + 'status' => 'ready', + 'share_token'=> bin2hex(random_bytes(8)), + ], $attrs)); + return [$user, $video]; + } +}