2026-08-12 00:00:49 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Tests\Feature\Livewire;
|
|
|
|
|
|
2026-08-13 17:48:17 +02:00
|
|
|
use App\Enums\ActivityTypeEnum;
|
2026-08-13 00:54:29 +02:00
|
|
|
use App\Enums\ApprovalStatusEnum;
|
2026-08-13 17:34:57 +02:00
|
|
|
use App\Enums\PublishStatusEnum;
|
2026-08-12 00:00:49 +02:00
|
|
|
use App\Livewire\Dashboard;
|
2026-08-13 17:48:17 +02:00
|
|
|
use App\Models\ActivityLog;
|
2026-08-12 00:00:49 +02:00
|
|
|
use App\Models\Article;
|
2026-08-12 00:32:46 +02:00
|
|
|
use App\Models\Feed;
|
2026-08-12 00:41:10 +02:00
|
|
|
use App\Models\PlatformChannel;
|
2026-08-13 00:54:29 +02:00
|
|
|
use App\Models\RouteArticle;
|
2026-08-12 00:00:49 +02:00
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
|
use Illuminate\Support\Carbon;
|
2026-08-13 18:06:49 +02:00
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-08-12 00:20:46 +02:00
|
|
|
use Livewire\Features\SupportTesting\Testable;
|
2026-08-12 00:00:49 +02:00
|
|
|
use Livewire\Livewire;
|
|
|
|
|
use Tests\TestCase;
|
|
|
|
|
|
|
|
|
|
class DashboardTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use RefreshDatabase;
|
|
|
|
|
|
2026-08-13 00:40:21 +02:00
|
|
|
public function test_it_defaults_to_the_current_month(): void
|
2026-08-12 00:00:49 +02:00
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
2026-08-13 00:40:21 +02:00
|
|
|
->assertSet('from', '2026-07-01')
|
|
|
|
|
->assertSet('to', '2026-07-31');
|
2026-08-12 00:00:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_recomputes_stats_when_the_range_changes(): void
|
|
|
|
|
{
|
|
|
|
|
Article::factory()->count(3)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->set('from', '2026-07-01')
|
|
|
|
|
->set('to', '2026-07-31')
|
|
|
|
|
->assertSee('3')
|
|
|
|
|
->set('from', '2026-08-01')
|
|
|
|
|
->set('to', '2026-08-31')
|
|
|
|
|
->assertSee('0');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_populates_the_range_when_a_preset_is_applied(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->call('applyPreset', 'month')
|
|
|
|
|
->assertSet('from', '2026-07-01')
|
|
|
|
|
->assertSet('to', '2026-07-31');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_applies_the_all_preset_as_a_bounded_range(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->call('applyPreset', 'all')
|
|
|
|
|
->assertSet('to', '2026-07-15');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_ignores_an_unknown_preset(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->call('applyPreset', 'fortnight')
|
2026-08-13 00:40:21 +02:00
|
|
|
->assertSet('from', '2026-07-01')
|
|
|
|
|
->assertSet('to', '2026-07-31')
|
2026-08-12 00:00:49 +02:00
|
|
|
->assertHasNoErrors();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_reports_an_inverted_range_against_the_end_date(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->set('from', '2026-07-31')
|
|
|
|
|
->set('to', '2026-07-01')
|
|
|
|
|
->assertHasErrors('to');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_blames_the_start_date_when_the_start_date_is_malformed(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->set('from', 'not-a-date')
|
|
|
|
|
->assertHasErrors('from')
|
|
|
|
|
->assertHasNoErrors('to');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_blames_the_end_date_when_the_end_date_is_malformed(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->set('to', 'not-a-date')
|
|
|
|
|
->assertHasErrors('to')
|
|
|
|
|
->assertHasNoErrors('from');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_reports_zero_stats_for_an_invalid_range(): void
|
|
|
|
|
{
|
|
|
|
|
Article::factory()->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->set('from', '2026-07-01')
|
|
|
|
|
->set('to', 'not-a-date')
|
|
|
|
|
->assertSet('rangeIsValid', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_marks_a_well_formed_range_as_valid(): void
|
|
|
|
|
{
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->set('from', '2026-07-01')
|
|
|
|
|
->set('to', '2026-07-31')
|
|
|
|
|
->assertSet('rangeIsValid', true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_renders_the_preset_buttons(): void
|
|
|
|
|
{
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->assertSee('This Month')
|
|
|
|
|
->assertSee('All Time');
|
|
|
|
|
}
|
2026-08-12 00:20:46 +02:00
|
|
|
|
|
|
|
|
public function test_it_renders_each_panel_in_its_own_island(): void
|
|
|
|
|
{
|
|
|
|
|
$html = Livewire::test(Dashboard::class)->html();
|
|
|
|
|
|
|
|
|
|
foreach (['system-overview', 'article-statistics'] as $island) {
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
1,
|
|
|
|
|
substr_count($html, "[if FRAGMENT:type=island|name={$island}|"),
|
|
|
|
|
"Expected exactly one [{$island}] island.",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param Testable<Dashboard> $component
|
|
|
|
|
*/
|
|
|
|
|
private function islandFragments(Testable $component): string
|
|
|
|
|
{
|
|
|
|
|
/** @var array<int, string> $fragments */
|
|
|
|
|
$fragments = $component->effects['islandFragments'] ?? [];
|
|
|
|
|
|
|
|
|
|
return implode("\n", $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_re_renders_the_article_statistics_island_when_the_range_changes(): void
|
|
|
|
|
{
|
|
|
|
|
Article::factory()->count(3)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*3\s*</s', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_reflects_a_narrowed_range_in_the_re_rendered_island(): void
|
|
|
|
|
{
|
|
|
|
|
Article::factory()->count(3)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-08-01', '2026-08-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*0\s*</s', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_refreshes_only_the_range_dependent_island(): void
|
|
|
|
|
{
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('name=article-statistics|', $fragments);
|
|
|
|
|
$this->assertStringNotContainsString('name=system-overview|', $fragments);
|
|
|
|
|
$this->assertStringNotContainsString('Active Feeds', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 00:32:46 +02:00
|
|
|
public function test_it_renders_the_articles_per_feed_breakdown_on_mount(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
$feed = Feed::factory()->create(['name' => 'Example Feed']);
|
|
|
|
|
|
|
|
|
|
Article::factory()->count(2)->create([
|
|
|
|
|
'feed_id' => $feed->id,
|
|
|
|
|
'created_at' => Carbon::parse('2026-07-15 09:00:00'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->assertSee('Example Feed')
|
|
|
|
|
->assertSee('Articles per Feed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_counts_articles_per_feed_for_the_selected_range(): void
|
|
|
|
|
{
|
|
|
|
|
$feed = Feed::factory()->create(['name' => 'Example Feed']);
|
|
|
|
|
|
|
|
|
|
Article::factory()->count(2)->create([
|
|
|
|
|
'feed_id' => $feed->id,
|
|
|
|
|
'created_at' => Carbon::parse('2026-07-10 12:00:00'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertMatchesRegularExpression('/Example Feed.*?>\s*2\s*/s', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_refreshes_the_articles_per_feed_island_when_the_range_changes(): void
|
|
|
|
|
{
|
|
|
|
|
Feed::factory()->create(['name' => 'Example Feed']);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('name=articles-per-feed|', $fragments);
|
|
|
|
|
$this->assertStringContainsString('Example Feed', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 00:41:10 +02:00
|
|
|
public function test_it_renders_the_publications_per_channel_breakdown_on_mount(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
PlatformChannel::factory()->create(['display_name' => 'Example Channel']);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->assertSee('Publications per Channel')
|
|
|
|
|
->assertSee('Example Channel');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_refreshes_the_publications_per_channel_island_when_the_range_changes(): void
|
|
|
|
|
{
|
|
|
|
|
PlatformChannel::factory()->create(['display_name' => 'Example Channel']);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('name=publications-per-channel|', $fragments);
|
|
|
|
|
$this->assertStringContainsString('Example Channel', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 00:20:46 +02:00
|
|
|
public function test_it_re_renders_the_article_statistics_island_when_a_preset_is_applied(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Article::factory()->count(2)->create(['created_at' => Carbon::parse('2026-07-15 09:00:00')]);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyPreset', 'month')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*2\s*</s', $fragments);
|
|
|
|
|
}
|
2026-08-13 00:10:34 +02:00
|
|
|
|
|
|
|
|
public function test_it_renders_the_trend_chart_on_mount(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
Article::factory()->create(['created_at' => Carbon::parse('2026-07-15 09:00:00')]);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->assertSee('Fetched vs Published')
|
|
|
|
|
->assertSee('trendChart(', false)
|
|
|
|
|
->assertSee('x-ref="canvas"', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_renders_the_trend_chart_in_its_own_island(): void
|
|
|
|
|
{
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('name=articles-trend|', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_refreshes_the_trend_island_when_the_range_changes(): void
|
|
|
|
|
{
|
|
|
|
|
Article::factory()->count(2)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('name=articles-trend|', $fragments);
|
|
|
|
|
$this->assertStringContainsString('2026-07-10', $fragments);
|
|
|
|
|
$this->assertStringContainsString('Fetched', $fragments);
|
|
|
|
|
$this->assertStringContainsString('Published', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_reports_a_too_wide_range_instead_of_charting_it(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyPreset', 'all')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('too wide to chart by day', $fragments);
|
|
|
|
|
$this->assertStringNotContainsString('x-ref="canvas"', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_reports_an_empty_range_separately_from_a_too_wide_one(): void
|
|
|
|
|
{
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('No articles or publications in this range.', $fragments);
|
|
|
|
|
$this->assertStringNotContainsString('too wide to chart by day', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_keys_the_chart_on_its_data_so_a_range_change_replaces_it(): void
|
|
|
|
|
{
|
|
|
|
|
Article::factory()->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
|
|
|
|
|
Article::factory()->create(['created_at' => Carbon::parse('2026-08-10 12:00:00')]);
|
|
|
|
|
|
|
|
|
|
$july = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$august = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-08-01', '2026-08-31')
|
|
|
|
|
);
|
|
|
|
|
|
2026-08-13 00:54:29 +02:00
|
|
|
preg_match('/wire:key="(chart-[a-f0-9]+)"/', $july, $julyKey);
|
|
|
|
|
preg_match('/wire:key="(chart-[a-f0-9]+)"/', $august, $augustKey);
|
2026-08-13 00:10:34 +02:00
|
|
|
|
|
|
|
|
$this->assertNotEmpty($julyKey);
|
|
|
|
|
$this->assertNotEmpty($augustKey);
|
|
|
|
|
$this->assertNotSame($julyKey[1], $augustKey[1]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 00:54:29 +02:00
|
|
|
public function test_it_keys_each_chart_separately_when_their_payloads_match(): void
|
|
|
|
|
{
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
preg_match_all('/wire:key="(chart-[a-f0-9]+)"/', $fragments, $keys);
|
|
|
|
|
|
|
|
|
|
$this->assertSame($keys[1], array_unique($keys[1]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_renders_the_approval_rate_chart_on_mount(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
RouteArticle::factory()->create([
|
|
|
|
|
'approval_status' => ApprovalStatusEnum::APPROVED,
|
|
|
|
|
'decided_at' => Carbon::parse('2026-07-15 09:00:00'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->assertSee('Approval Rate')
|
|
|
|
|
->assertSee('trendChart(', false)
|
|
|
|
|
->assertSee('x-ref="canvas"', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_refreshes_the_approval_rate_island_when_the_range_changes(): void
|
|
|
|
|
{
|
|
|
|
|
RouteArticle::factory()->create([
|
|
|
|
|
'approval_status' => ApprovalStatusEnum::APPROVED,
|
|
|
|
|
'decided_at' => Carbon::parse('2026-07-10 12:00:00'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('name=approval-rate|', $fragments);
|
|
|
|
|
$this->assertStringContainsString('Approval Rate', $fragments);
|
|
|
|
|
$this->assertStringContainsString('2026-07-10', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_charts_a_day_where_everything_was_rejected(): void
|
|
|
|
|
{
|
|
|
|
|
RouteArticle::factory()->create([
|
|
|
|
|
'approval_status' => ApprovalStatusEnum::REJECTED,
|
|
|
|
|
'decided_at' => Carbon::parse('2026-07-10 12:00:00'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringNotContainsString('No approval decisions in this range.', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_reports_a_range_with_no_approval_decisions(): void
|
|
|
|
|
{
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('No approval decisions in this range.', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 00:10:34 +02:00
|
|
|
public function test_it_renders_an_empty_trend_for_an_invalid_range(): void
|
|
|
|
|
{
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->set('from', 'not-a-date')
|
|
|
|
|
->assertSee('Fetched vs Published')
|
|
|
|
|
->assertDontSee('x-ref="canvas"', false);
|
|
|
|
|
}
|
2026-08-13 17:34:57 +02:00
|
|
|
|
|
|
|
|
private function publishAttempt(PublishStatusEnum $status, string $updatedAt): void
|
|
|
|
|
{
|
|
|
|
|
$routeArticle = RouteArticle::factory()->create(['publish_status' => $status]);
|
|
|
|
|
|
|
|
|
|
// Model::update() would re-stamp updated_at to now(); bypass via the query builder.
|
|
|
|
|
RouteArticle::query()
|
|
|
|
|
->whereKey($routeArticle->getKey())
|
|
|
|
|
->update(['updated_at' => Carbon::parse($updatedAt)]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_renders_the_publish_success_rate_chart_on_mount(): void
|
|
|
|
|
{
|
|
|
|
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
|
|
|
|
|
|
|
|
|
$this->publishAttempt(PublishStatusEnum::PUBLISHED, '2026-07-15 09:00:00');
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->assertSee('Publish Success Rate')
|
|
|
|
|
->assertSee('trendChart(', false)
|
|
|
|
|
->assertSee('x-ref="canvas"', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_refreshes_the_publish_success_rate_island_when_the_range_changes(): void
|
|
|
|
|
{
|
|
|
|
|
$this->publishAttempt(PublishStatusEnum::PUBLISHED, '2026-07-10 12:00:00');
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('name=publish-success-rate|', $fragments);
|
|
|
|
|
$this->assertStringContainsString('Publish Success Rate', $fragments);
|
|
|
|
|
$this->assertStringContainsString('2026-07-10', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_charts_a_day_where_every_publish_failed(): void
|
|
|
|
|
{
|
|
|
|
|
$this->publishAttempt(PublishStatusEnum::ERROR, '2026-07-10 12:00:00');
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringNotContainsString('No publish attempts in this range.', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_reports_a_range_with_no_publish_attempts(): void
|
|
|
|
|
{
|
|
|
|
|
$this->publishAttempt(PublishStatusEnum::SKIPPED, '2026-07-10 12:00:00');
|
|
|
|
|
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('No publish attempts in this range.', $fragments);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 17:48:17 +02:00
|
|
|
public function test_it_lists_the_most_recent_activity(): void
|
|
|
|
|
{
|
|
|
|
|
ActivityLog::factory()->type(ActivityTypeEnum::PUBLISH)->loggedAt(Carbon::parse('2026-07-10 09:00:00'))->create(['message' => 'Older entry']);
|
|
|
|
|
ActivityLog::factory()->type(ActivityTypeEnum::ERROR)->loggedAt(Carbon::parse('2026-07-12 09:00:00'))->create(['message' => 'Newer entry']);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->assertSee('Recent Activity')
|
|
|
|
|
->assertSee('Newer entry')
|
|
|
|
|
->assertSee('Older entry')
|
|
|
|
|
->assertSeeInOrder(['Newer entry', 'Older entry']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_caps_the_activity_panel_at_five_entries(): void
|
|
|
|
|
{
|
|
|
|
|
foreach (range(1, 8) as $index) {
|
|
|
|
|
ActivityLog::factory()
|
|
|
|
|
->loggedAt(Carbon::parse('2026-07-10 09:00:00')->addMinutes($index))
|
|
|
|
|
->create(['message' => "Entry {$index}"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$component = Livewire::test(Dashboard::class);
|
|
|
|
|
|
|
|
|
|
$component->assertSee('Entry 8')->assertSee('Entry 4');
|
|
|
|
|
$component->assertDontSee('Entry 3');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_the_activity_panel_ignores_the_global_range(): void
|
|
|
|
|
{
|
|
|
|
|
ActivityLog::factory()
|
|
|
|
|
->loggedAt(Carbon::parse('2026-07-10 09:00:00'))
|
|
|
|
|
->create(['message' => 'Activity outside the range']);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->call('applyRange', '2026-08-01', '2026-08-31')
|
|
|
|
|
->assertSee('Activity outside the range');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_reports_an_empty_activity_panel(): void
|
|
|
|
|
{
|
|
|
|
|
Livewire::test(Dashboard::class)->assertSee('Nothing has happened yet.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_links_the_activity_panel_to_the_activity_page(): void
|
|
|
|
|
{
|
|
|
|
|
Livewire::test(Dashboard::class)->assertSee(route('activity'), false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_shows_the_feed_name_for_an_article_subject(): void
|
|
|
|
|
{
|
|
|
|
|
$feed = Feed::factory()->create(['name' => 'Belga News']);
|
|
|
|
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
|
|
|
|
|
|
|
|
|
ActivityLog::factory()
|
|
|
|
|
->forSubject($article)
|
|
|
|
|
->create(['message' => 'Fetched something']);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)
|
|
|
|
|
->assertSee('Fetched something')
|
|
|
|
|
->assertSee('Belga News');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_renders_an_activity_entry_without_a_subject(): void
|
|
|
|
|
{
|
|
|
|
|
ActivityLog::factory()->create(['message' => 'Subjectless entry']);
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)->assertSee('Subjectless entry');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_renders_an_activity_entry_whose_subject_was_deleted(): void
|
|
|
|
|
{
|
|
|
|
|
$article = Article::factory()->create();
|
|
|
|
|
|
|
|
|
|
ActivityLog::factory()->forSubject($article)->create(['message' => 'Orphaned entry']);
|
|
|
|
|
|
|
|
|
|
$article->delete();
|
|
|
|
|
|
|
|
|
|
Livewire::test(Dashboard::class)->assertSee('Orphaned entry');
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 18:06:49 +02:00
|
|
|
private function dashboardQueryCount(int $activityEntries): int
|
|
|
|
|
{
|
|
|
|
|
$feed = Feed::factory()->create();
|
|
|
|
|
|
|
|
|
|
foreach (range(1, $activityEntries) as $index) {
|
|
|
|
|
$article = Article::factory()->create(['feed_id' => $feed->id]);
|
|
|
|
|
ActivityLog::factory()->forSubject($article)->create(['message' => "Entry {$index}"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DB::flushQueryLog();
|
|
|
|
|
DB::enableQueryLog();
|
|
|
|
|
Livewire::test(Dashboard::class);
|
|
|
|
|
$queries = count(DB::getQueryLog());
|
|
|
|
|
DB::disableQueryLog();
|
|
|
|
|
|
|
|
|
|
return $queries;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test_it_does_not_fan_out_queries_per_activity_entry(): void
|
|
|
|
|
{
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
$this->dashboardQueryCount(1),
|
|
|
|
|
$this->dashboardQueryCount(20),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 17:34:57 +02:00
|
|
|
public function test_every_range_dependent_island_re_renders_on_a_range_change(): void
|
|
|
|
|
{
|
|
|
|
|
$fragments = $this->islandFragments(
|
|
|
|
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
foreach (['articles-trend', 'approval-rate', 'publish-success-rate', 'articles-per-feed', 'publications-per-channel'] as $island) {
|
|
|
|
|
$this->assertStringContainsString("name={$island}|", $fragments);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-12 00:00:49 +02:00
|
|
|
}
|