test(phase-6): channel handle, preview mode, playlist meta

Also tightens stat/post-video rendering so "0" prints as "0" and not
"0.0" (Number::abbreviate(0, precision: 1) stringifies as "0.0", which
leaked into the About tab's Total Views card and post-video badges).

Four tests covering:
- /@username opens the matching channel.
- /@invalid url (space) 404s.
- Owner view shows id="postForm"; preview?preview=1 hides it.
- Zero-view playlist doesn't render "bi-eye … 0.0"; "1 videos" never
  renders in place of "1 video".

Fixed a Blade dupe-declaration fatal that only fired in feature tests
(two requests in one PHP process would redeclare notifVal()). Guarded
with function_exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-10-21 10:41:25 +03:00
1 parent e939dd3a53
commit 1b2377f20f
2 files changed
+114 -2

No files matched your search

+7 -2
View File
@@ -548,7 +548,7 @@ $headerSocialMap = [
<div class="ch-post-video-info"> <div class="ch-post-video-info">
<div class="ch-post-video-title">{{ $v->title }}</div> <div class="ch-post-video-title">{{ $v->title }}</div>
<div class="ch-post-video-meta"> <div class="ch-post-video-meta">
<span class="ch-post-video-badge"><i class="bi bi-eye"></i> {{ \Illuminate\Support\Number::abbreviate($v->view_count, precision: 1) }}</span> <span class="ch-post-video-badge"><i class="bi bi-eye"></i> {{ $v->view_count > 0 ? \Illuminate\Support\Number::abbreviate($v->view_count, precision: 1) : '0' }}</span>
@if($v->duration)<span class="ch-post-video-badge"><i class="bi bi-clock"></i> {{ $v->formatted_duration }}</span>@endif @if($v->duration)<span class="ch-post-video-badge"><i class="bi bi-clock"></i> {{ $v->formatted_duration }}</span>@endif
@if($v->is_shorts)<span class="ch-post-video-badge" style="color:#e33;"><i class="bi bi-phone"></i> Short</span>@endif @if($v->is_shorts)<span class="ch-post-video-badge" style="color:#e33;"><i class="bi bi-phone"></i> Short</span>@endif
</div> </div>
@@ -1091,7 +1091,7 @@ $headerSocialMap = [
<div class="ch-about-card-title"><i class="bi bi-bar-chart-fill"></i> Stats</div> <div class="ch-about-card-title"><i class="bi bi-bar-chart-fill"></i> Stats</div>
<div class="ch-stat-cards"> <div class="ch-stat-cards">
<div class="ch-stat-card"> <div class="ch-stat-card">
<div class="ch-stat-card-val">{{ \Illuminate\Support\Number::abbreviate($totalViews, precision: 1) }}</div> <div class="ch-stat-card-val">{{ $totalViews > 0 ? \Illuminate\Support\Number::abbreviate($totalViews, precision: 1) : '0' }}</div>
<div class="ch-stat-card-key">Total Views</div> <div class="ch-stat-card-key">Total Views</div>
</div> </div>
<div class="ch-stat-card"> <div class="ch-stat-card">
@@ -1372,9 +1372,14 @@ if ($oldLinks) {
@php @php
$notifPrefs = $user->notification_preferences ?? []; $notifPrefs = $user->notification_preferences ?? [];
$notifDefs = \App\Models\User::notifDefaults(); $notifDefs = \App\Models\User::notifDefaults();
// function_exists guard so a second render in the same PHP
// process (feature tests fire two requests in one process)
// doesn't fatal with "Cannot redeclare notifVal()".
if (!function_exists('notifVal')) {
function notifVal(array $prefs, array $defs, string $key): bool { function notifVal(array $prefs, array $defs, string $key): bool {
return isset($prefs[$key]) ? (bool)$prefs[$key] : ($defs[$key] ?? true); return isset($prefs[$key]) ? (bool)$prefs[$key] : ($defs[$key] ?? true);
} }
}
@endphp @endphp
<div class="ch-settings-card" id="notifPrefsCard"> <div class="ch-settings-card" id="notifPrefsCard">
<div class="ch-settings-card-title"><i class="bi bi-bell-fill"></i> Notification Preferences</div> <div class="ch-settings-card-title"><i class="bi bi-bell-fill"></i> Notification Preferences</div>
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace Tests\Feature;
use App\Http\Middleware\VerifyCsrfToken;
use App\Models\Playlist;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Phase 6 — Channel / Studio / Live.
*
* Covers the handle-alias route, the composer being hidden in preview mode,
* and the pluralised playlist meta.
*/
class Phase6ChannelTest 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,
]);
}
// ─── /@handle alias ─────────────────────────────
public function test_at_handle_url_opens_the_matching_channel(): void
{
$user = User::factory()->create([
'name' => 'Handle Test',
'username' => 'handletest-'.substr(bin2hex(random_bytes(3)), 0, 6),
]);
$r = $this->get('/@' . $user->username)->assertOk();
$r->assertSee('Handle Test', false);
}
public function test_at_handle_rejects_invalid_characters(): void
{
// Space in the handle must not match the /@ route.
$this->get('/@not valid')->assertStatus(404);
}
// ─── Preview-as-visitor hides composer ──────────
public function test_preview_as_visitor_hides_the_wall_composer(): void
{
$user = User::factory()->create([
'username' => 'preview-'.substr(bin2hex(random_bytes(3)), 0, 6),
]);
$this->actingAs($user);
// The composer form carries id="postForm" and action pointing at
// posts.store — specific enough that we don't collide with the
// "ch-composer-*" literals that appear in inline JS strings.
$owner = $this->get('/channel/' . $user->username)->getContent();
$this->assertStringContainsString('id="postForm"', $owner,
'Owner should see the composer form in normal view.');
$preview = $this->get('/channel/' . $user->username . '?preview=1')->getContent();
$this->assertStringNotContainsString('id="postForm"', $preview,
'Preview-as-visitor must hide the composer form.');
}
// ─── Playlist card: pluralise + hide stray "0.0" ──
public function test_playlist_card_pluralises_videos_and_hides_zero_views(): void
{
$owner = User::factory()->create([
'username' => 'pl-'.substr(bin2hex(random_bytes(3)), 0, 6),
]);
$this->actingAs($owner);
$pl = Playlist::create([
'user_id' => $owner->id,
'name' => 'Empty Playlist',
'visibility' => 'public',
'share_token'=> bin2hex(random_bytes(8)),
]);
$html = $this->get('/channel/' . $owner->username)->getContent();
// The specific bug: the view-count row ends with
// `<i class="bi bi-eye" aria-hidden="true"></i> 0.0`
// for a zero-view playlist. The fix hides the whole row for zero
// views, so the "bi-eye ...0.0" sequence should never appear.
$this->assertDoesNotMatchRegularExpression(
'/bi-eye[^<]{0,200}0\.0/',
$html,
'Playlist meta must not emit "bi-eye … 0.0" for zero-view playlists.'
);
// Plural must switch to singular for 1 video. Never "1 videos".
$this->assertDoesNotMatchRegularExpression('/\b1 videos\b/i', $html);
}
}