Allow heading tags (h1-h6) in ContentSanitizer so news editor headings render
This commit is contained in:
@@ -10,6 +10,7 @@ use App\Models\AcademyCourse;
|
||||
use App\Models\AcademyEvent;
|
||||
use App\Models\AcademyLesson;
|
||||
use App\Models\AcademyLike;
|
||||
use App\Models\AcademyPromptPack;
|
||||
use App\Models\AcademyPromptTemplate;
|
||||
use App\Models\AcademySave;
|
||||
use App\Models\AcademySearchLog;
|
||||
@@ -23,6 +24,7 @@ use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Inertia\Testing\AssertableInertia;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class AcademyAnalyticsTest extends TestCase
|
||||
@@ -406,6 +408,80 @@ final class AcademyAnalyticsTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_prompt_library_index_exposes_dedicated_page_analytics_content_type(): void
|
||||
{
|
||||
$this->createPrompt('library-analytics-prompt');
|
||||
|
||||
$this->get(route('academy.prompts.index'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Academy/List')
|
||||
->where('analytics.contentType', AcademyAnalyticsContentType::PROMPT_LIBRARY)
|
||||
->where('analytics.contentId', null)
|
||||
->where('analytics.pageName', 'academy_prompts_index')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_popular_prompts_page_exposes_dedicated_page_analytics_content_type(): void
|
||||
{
|
||||
$prompt = $this->createPrompt('popular-page-prompt');
|
||||
$this->createMetric(AcademyAnalyticsContentType::PROMPT, $prompt->id, [
|
||||
'views' => 18,
|
||||
'prompt_copies' => 4,
|
||||
'popularity_score' => 51.2,
|
||||
]);
|
||||
|
||||
$this->get(route('academy.prompts.popular'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Academy/List')
|
||||
->where('analytics.contentType', AcademyAnalyticsContentType::PROMPT_POPULAR)
|
||||
->where('analytics.contentId', null)
|
||||
->where('analytics.pageName', 'academy_prompts_popular')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_popular_prompts_page_exposes_selected_period_in_analytics_metadata(): void
|
||||
{
|
||||
$prompt = $this->createPrompt('popular-period-analytics-prompt');
|
||||
$this->createMetric(AcademyAnalyticsContentType::PROMPT, $prompt->id, [
|
||||
'views' => 28,
|
||||
'prompt_copies' => 6,
|
||||
'popularity_score' => 64.4,
|
||||
]);
|
||||
|
||||
$this->get(route('academy.prompts.popular', ['period' => '7d']))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Academy/List')
|
||||
->where('analytics.metadata.period', '7d')
|
||||
->where('analytics.metadata.period_days', 7)
|
||||
->where('analytics.trackingKey', 'period:7d')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_prompt_pack_library_index_exposes_dedicated_page_analytics_content_type(): void
|
||||
{
|
||||
AcademyPromptPack::query()->create([
|
||||
'title' => 'Starter Pack',
|
||||
'slug' => 'starter-pack',
|
||||
'excerpt' => 'A free pack.',
|
||||
'description' => 'Pack description.',
|
||||
'access_level' => 'free',
|
||||
'active' => true,
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
$this->get(route('academy.packs.index'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Academy/List')
|
||||
->where('analytics.contentType', AcademyAnalyticsContentType::PROMPT_PACK_LIBRARY)
|
||||
->where('analytics.contentId', null)
|
||||
->where('analytics.pageName', 'academy_packs_index')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_rollup_counts_search_result_clicks_for_clicked_content(): void
|
||||
{
|
||||
$prompt = $this->createPrompt('rollup-search-click-prompt');
|
||||
@@ -948,6 +1024,151 @@ final class AcademyAnalyticsTest extends TestCase
|
||||
$this->assertSame(20.0, (float) $metric->conversion_score);
|
||||
}
|
||||
|
||||
public function test_admin_prompt_library_analytics_page_uses_dedicated_prompt_library_filter(): void
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get(route('admin.academy.analytics.prompt-library'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Admin/Academy/AnalyticsContent')
|
||||
->where('title', 'Prompt library analytics')
|
||||
->where('filters.content_type', AcademyAnalyticsContentType::PROMPT_LIBRARY)
|
||||
->where('contentTypeOptions', fn ($options): bool => collect($options)->contains(
|
||||
fn (array $option): bool => ($option['value'] ?? null) === AcademyAnalyticsContentType::PROMPT_LIBRARY
|
||||
&& ($option['label'] ?? null) === 'Prompt library'
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
public function test_admin_prompt_library_analytics_page_includes_prompt_library_summary(): void
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
$this->createMetric(AcademyAnalyticsContentType::PROMPT_LIBRARY, null, [
|
||||
'views' => 42,
|
||||
'unique_visitors' => 18,
|
||||
'engaged_views' => 9,
|
||||
'scroll_50' => 12,
|
||||
'scroll_100' => 4,
|
||||
'avg_engaged_seconds' => 31,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get(route('admin.academy.analytics.prompt-library'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Admin/Academy/AnalyticsContent')
|
||||
->where('summary.views', 42)
|
||||
->where('summary.uniqueVisitors', 18)
|
||||
->where('summary.engagedViews', 9)
|
||||
->where('summary.avgEngagedSeconds', 31)
|
||||
->where('summary.engagementRate', 50)
|
||||
->where('summary.deepScrollRate', 22.2)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_admin_analytics_overview_includes_prompt_library_trend(): void
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
$today = now()->toDateString();
|
||||
$yesterday = now()->subDay()->toDateString();
|
||||
|
||||
$this->createMetric(AcademyAnalyticsContentType::PROMPT_LIBRARY, null, [
|
||||
'date' => $today,
|
||||
'views' => 40,
|
||||
'unique_visitors' => 20,
|
||||
'engaged_views' => 10,
|
||||
'popularity_score' => 55,
|
||||
]);
|
||||
|
||||
$this->createMetric(AcademyAnalyticsContentType::PROMPT_LIBRARY, null, [
|
||||
'date' => $yesterday,
|
||||
'views' => 20,
|
||||
'unique_visitors' => 10,
|
||||
'engaged_views' => 5,
|
||||
'popularity_score' => 25,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get(route('admin.academy.analytics.overview', [
|
||||
'range' => 'custom',
|
||||
'from' => $today,
|
||||
'to' => $today,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Admin/Academy/AnalyticsOverview')
|
||||
->where('promptLibraryTrend.current.views', 40)
|
||||
->where('promptLibraryTrend.current.uniqueVisitors', 20)
|
||||
->where('promptLibraryTrend.current.engagementRate', 50)
|
||||
->where('promptLibraryTrend.previous.views', 20)
|
||||
->where('promptLibraryTrend.deltas.views', 100)
|
||||
->where('promptLibraryTrend.deltas.engagementRate', 0)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_admin_analytics_overview_includes_popular_prompt_period_usage(): void
|
||||
{
|
||||
$admin = User::factory()->create(['role' => 'admin']);
|
||||
|
||||
AcademyEvent::query()->create([
|
||||
'event_type' => AcademyAnalyticsEventType::PAGE_VIEW,
|
||||
'content_type' => AcademyAnalyticsContentType::PROMPT_POPULAR,
|
||||
'visitor_id' => 'visitor-30-a',
|
||||
'is_logged_in' => false,
|
||||
'is_subscriber' => false,
|
||||
'is_admin' => false,
|
||||
'is_bot' => false,
|
||||
'is_crawler' => false,
|
||||
'is_suspicious' => false,
|
||||
'metadata' => ['period' => '30d', 'period_days' => 30],
|
||||
'occurred_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
AcademyEvent::query()->create([
|
||||
'event_type' => AcademyAnalyticsEventType::PAGE_VIEW,
|
||||
'content_type' => AcademyAnalyticsContentType::PROMPT_POPULAR,
|
||||
'visitor_id' => 'visitor-30-b',
|
||||
'is_logged_in' => false,
|
||||
'is_subscriber' => false,
|
||||
'is_admin' => false,
|
||||
'is_bot' => false,
|
||||
'is_crawler' => false,
|
||||
'is_suspicious' => false,
|
||||
'metadata' => ['period' => '30d', 'period_days' => 30],
|
||||
'occurred_at' => now()->subMinutes(30),
|
||||
]);
|
||||
|
||||
AcademyEvent::query()->create([
|
||||
'event_type' => AcademyAnalyticsEventType::PAGE_VIEW,
|
||||
'content_type' => AcademyAnalyticsContentType::PROMPT_POPULAR,
|
||||
'visitor_id' => 'visitor-7-a',
|
||||
'is_logged_in' => false,
|
||||
'is_subscriber' => false,
|
||||
'is_admin' => false,
|
||||
'is_bot' => false,
|
||||
'is_crawler' => false,
|
||||
'is_suspicious' => false,
|
||||
'metadata' => ['period' => '7d', 'period_days' => 7],
|
||||
'occurred_at' => now()->subMinutes(10),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)
|
||||
->get(route('admin.academy.analytics.overview'))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Admin/Academy/AnalyticsOverview')
|
||||
->where('popularPromptPeriodUsage.totalViews', 3)
|
||||
->where('popularPromptPeriodUsage.totalVisitors', 3)
|
||||
->where('popularPromptPeriodUsage.periods.0.period', '30d')
|
||||
->where('popularPromptPeriodUsage.periods.0.views', 2)
|
||||
->where('popularPromptPeriodUsage.periods.0.share', 66.7)
|
||||
->where('popularPromptPeriodUsage.periods.1.period', '7d')
|
||||
->where('popularPromptPeriodUsage.periods.1.views', 1));
|
||||
}
|
||||
|
||||
public function test_prune_events_command_removes_only_old_raw_events(): void
|
||||
{
|
||||
$oldEvent = AcademyEvent::query()->create([
|
||||
|
||||
Reference in New Issue
Block a user