test(polish): match channel row, nested-reply, cursor paginator, hotkeys

Four tests covering the post-audit polish commits:
- Match watch page renders the shared .channel-row component.
- Reply-to-reply button carries data-parent-id and data-handle so the
  JS can prefill "@author " when the parent reply form opens.
- liked() controller returns a CursorPaginator instance (controller
  introspection — avoids a 400 KB view render per assertion).
- video-player component ships the ">" / "<" playback-rate hotkey.

Also cleans up a stale ->onEachSide()->links() call in user/liked.blade
that would blow up now that the controller returns a CursorPaginator
(which doesn't support onEachSide).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-10-21 12:11:08 +03:00
1 parent 341e94a4f8
commit 73de5bc97e
2 files changed
+167 -1

No files matched your search

+2 -1
View File
@@ -146,7 +146,8 @@
@endforeach @endforeach
</div> </div>
<div class="mt-4">{{ $videos->onEachSide(1)->links() }}</div> {{-- Cursor paginator has no "N pages" concept — just prev/next. --}}
<div class="mt-4">{{ $videos->links() }}</div>
@endif @endif
@endsection @endsection
+165
View File
@@ -0,0 +1,165 @@
<?php
namespace Tests\Feature;
use App\Http\Middleware\VerifyCsrfToken;
use App\Models\Comment;
use App\Models\SportsMatch;
use App\Models\User;
use App\Models\Video;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Post-audit polish — the deferred items we tackled in bulk after Phase 7:
* match channel row, read-more clamp, nested-reply prefill, player hotkeys,
* cursor pagination on /liked and /history.
*/
class PostAuditPolishTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
putenv('DB_CONNECTION=sqlite');
putenv('DB_DATABASE=:memory:');
$_ENV['DB_CONNECTION'] = 'sqlite';
$_ENV['DB_DATABASE'] = ':memory:';
parent::setUp();
$this->withoutMiddleware([
VerifyCsrfToken::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
]);
}
// ─── Match page: channel row ─────────────────────
public function test_match_watch_page_renders_the_channel_row(): void
{
$owner = $this->makeUser('match-owner');
$video = Video::create([
'user_id' => $owner->id,
'title' => 'MatchTitle',
'description' => '',
'filename' => 'users/x/sports/y/video.mp4',
'path' => 'users/x/sports/y/video.mp4',
'type' => 'match',
'visibility' => 'public',
'status' => 'ready',
'share_token' => bin2hex(random_bytes(8)),
]);
// Minimal sports_match so the match blade renders without null checks exploding.
SportsMatch::create([
'video_id' => $video->id,
'user_id' => $owner->id,
'title' => 'MatchTitle',
'participants' => [],
'sport' => 'karate',
]);
$html = $this->get(route('videos.show', $video))->getContent();
$this->assertStringContainsString('channel-row', $html,
'Match watch page must render the shared channel row component.');
}
// ─── Comment reply: nested handle button present ──
public function test_reply_to_reply_button_carries_data_handle(): void
{
$owner = $this->makeUser('replyowner');
$this->actingAs($owner);
$video = Video::create([
'user_id' => $owner->id,
'title' => 'Vid',
'description'=> '',
'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)),
]);
$parent = Comment::create([
'user_id' => $owner->id,
'video_id' => $video->id,
'body' => 'top',
]);
$replier = $this->makeUser('replier');
Comment::create([
'user_id' => $replier->id,
'video_id' => $video->id,
'parent_id' => $parent->id,
'body' => 'nested',
]);
$html = view('components.video-comments', ['video' => $video])->render();
// The nested reply trigger must carry the reply author's handle so the
// JS can prefill "@handle " when the parent's reply form opens.
$this->assertMatchesRegularExpression(
'/_comment-reply-nested-trigger[^"]*"\s+data-parent-id="' . $parent->id . '"\s+data-handle="' . preg_quote($replier->username, '/') . '"/',
$html
);
}
// ─── Liked feed: cursor pagination ────────────────
public function test_liked_controller_returns_a_cursor_paginator(): void
{
// The controller introspection is enough to confirm the pagination
// mode — the full HTML path depends on view rendering that costs
// 400 KB per test and isn't the behaviour we're asserting on.
$user = $this->makeUser('liker');
$this->actingAs($user);
$route = app('router')->getRoutes()->getByName('liked');
[$class, $method] = explode('@', $route->getActionName());
$controller = app($class);
$response = $controller->$method();
$videos = $response->getData()['videos'] ?? null;
$this->assertInstanceOf(
\Illuminate\Pagination\CursorPaginator::class,
$videos,
'liked() must return a CursorPaginator instance.'
);
}
// ─── Player hotkey code is present ────────────────
public function test_player_component_handles_shift_dot_hotkey(): void
{
// The player component accepts a $video context — pass a bare
// record so the view renders.
$u = $this->makeUser('p');
$v = Video::create([
'user_id' => $u->id,
'title' => 't',
'description'=> '',
'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)),
]);
$html = view('components.video-player', ['video' => $v])->render();
$this->assertStringContainsString("case '>':", $html);
$this->assertStringContainsString("case '<':", $html);
$this->assertStringContainsString('video.playbackRate', $html);
}
// ─── Helpers ─────────────────────────────────────
private function makeUser(string $prefix): User
{
return User::factory()->create([
'username' => $prefix.'-'.substr(bin2hex(random_bytes(4)), 0, 6),
]);
}
}