clusev/tests/Feature/VersionsChangelogTest.php

161 lines
5.9 KiB
PHP

<?php
namespace Tests\Feature;
use App\Livewire\Versions\Index;
use App\Models\User;
use App\Services\DeploymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Tests\TestCase;
/**
* The changelog browser: releases are grouped into major.minor series (a left rail / pill row)
* and the active series is paginated so a long history never becomes an endless scroll. These
* tests drive the real CHANGELOG.md, asserting structural invariants (ordering, paging, defaults)
* rather than specific version numbers, so they survive new releases.
*/
class VersionsChangelogTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Cache::flush();
app(DeploymentService::class)->clearUpdateRequest();
$this->actingAs(User::factory()->create(['must_change_password' => false]));
}
protected function tearDown(): void
{
app(DeploymentService::class)->clearUpdateRequest();
parent::tearDown();
}
public function test_releases_are_grouped_into_series_ordered_newest_first(): void
{
Livewire::test(Index::class)->assertViewHas('seriesList', function ($series): bool {
if (! is_array($series) || count($series) < 2) {
return false;
}
// Newest series first: each key sorts >= the next (compare on "X.Y.0").
for ($i = 0; $i < count($series) - 1; $i++) {
if (version_compare($series[$i]['key'].'.0', $series[$i + 1]['key'].'.0', '<')) {
return false;
}
}
return true;
});
}
public function test_default_active_series_is_the_newest_one(): void
{
$firstKey = null;
$component = Livewire::test(Index::class)
->assertViewHas('seriesList', function ($series) use (&$firstKey): bool {
$firstKey = $series[0]['key'] ?? null;
return true;
});
$component->assertViewHas('activeKey', fn ($key): bool => $key === $firstKey);
}
public function test_an_unknown_selected_series_falls_back_to_the_newest(): void
{
Livewire::test(Index::class)
->call('selectSeries', '99.99')
->assertViewHas('activeKey', fn ($key): bool => $key !== '99.99' && $key !== '');
}
public function test_selecting_a_series_switches_the_visible_releases_and_resets_the_page(): void
{
// Default view shows the newest series, so the very first release (v0.1.0) is NOT visible…
Livewire::test(Index::class)->assertDontSee('v0.1.0');
// …selecting the 0.1 series brings it into view and jumps back to page 1.
Livewire::test(Index::class)
->set('changelogPage', 3)
->call('selectSeries', '0.1')
->assertSet('series', '0.1')
->assertSet('changelogPage', 1)
->assertSee('v0.1.0');
}
public function test_a_long_series_is_paginated_to_the_per_page_limit(): void
{
Livewire::test(Index::class)
->call('selectSeries', '0.9')
->assertViewHas('totalPages', fn ($total): bool => $total > 1)
->assertViewHas('pagedReleases', fn ($releases): bool => is_array($releases) && count($releases) <= 8);
}
public function test_paging_shows_a_different_slice_of_the_series(): void
{
$pageOne = null;
Livewire::test(Index::class)
->call('selectSeries', '0.9')
->assertViewHas('pagedReleases', function ($releases) use (&$pageOne): bool {
$pageOne = array_column($releases, 'version');
return true;
});
Livewire::test(Index::class)
->call('selectSeries', '0.9')
->call('gotoChangelogPage', 2)
->assertViewHas('pagedReleases', function ($releases) use (&$pageOne): bool {
$pageTwo = array_column($releases, 'version');
// Page 2 is non-empty and disjoint from page 1.
return $pageTwo !== [] && array_intersect($pageTwo, $pageOne) === [];
});
}
public function test_an_out_of_range_page_is_clamped(): void
{
Livewire::test(Index::class)
->call('selectSeries', '0.9')
->call('gotoChangelogPage', 9999)
->assertViewHas('changelogPage', fn ($page): bool => $page >= 1)
->assertViewHas('pagedReleases', fn ($releases): bool => is_array($releases) && count($releases) >= 1)
->assertSee(__('versions.changelog_title'));
}
public function test_the_installed_series_is_resolved_from_the_running_version(): void
{
config()->set('clusev.version', '0.7.3');
Livewire::test(Index::class)->assertViewHas('currentSeries', '0.7');
}
public function test_total_release_count_is_passed_for_the_panel_subtitle(): void
{
Livewire::test(Index::class)->assertViewHas('totalReleases', fn ($count): bool => is_int($count) && $count >= 1);
}
public function test_series_and_page_are_deep_linkable_via_the_query_string(): void
{
// ?series= + ?page= drive the view so a series/page is shareable and survives reload.
Livewire::withQueryParams(['series' => '0.9', 'page' => 2])
->test(Index::class)
->assertSet('series', '0.9')
->assertSet('changelogPage', 2);
}
public function test_chevron_right_icon_renders_a_real_path(): void
{
// The accordion disclosure caret and the pagination "Next" button both use x-icon
// name="chevron-right"; a missing map entry renders an empty <svg> (invisible affordance).
$html = Blade::render('<x-icon name="chevron-right" />');
$this->assertStringContainsString('<path', $html, 'chevron-right must resolve to a real Lucide path, not an empty svg');
}
}