Files
coolify/tests/Feature/TrafficAnalytics/ChartTokensTest.php
T
Andras Bacsai a25231683e feat(analytics): add chart categories, loading states, and apply action
Pass series bucket timestamps to sparkline payloads so charts align with the selected range. Show loading overlays on analytics filters and range tabs, restyle the analytics header, and replace Sentinel custom-image autosave with an Apply and restart button so the unsaved bar no longer flashes.
2026-09-08 13:24:39 +02:00

166 lines
6.5 KiB
PHP

<?php
// Guards the section-C tokenization: the analytics charts must read their colors
// from the shared --chart-* design tokens, not from inlined hex arrays.
it('defines the chart design tokens in the app stylesheet', function () {
$css = file_get_contents(base_path('resources/css/app.css'));
foreach ([
'--chart-status-2xx',
'--chart-status-3xx',
'--chart-status-4xx',
'--chart-status-5xx',
'--chart-geo-1',
'--chart-geo-2',
'--chart-geo-3',
'--chart-geo-4',
'--chart-geo-5',
'--chart-geo-empty',
] as $token) {
expect($css)->toContain($token);
}
// Dark overrides must exist so the palette is a selected dark theme, not a flip.
expect($css)->toContain('.dark {');
});
it('no longer hardcodes status color hex arrays in the analytics views', function () {
$views = [
base_path('resources/views/livewire/analytics.blade.php'),
base_path('resources/views/livewire/project/application/analytics.blade.php'),
];
foreach ($views as $view) {
$contents = file_get_contents($view);
expect($contents)->not->toContain('statusColorsLight');
expect($contents)->not->toContain('statusColorsDark');
// The chart lives in the shared partial; the views just pull it in.
expect($contents)->toContain("@include('livewire.traffic._requests-chart')");
}
});
it('reads its accent color from a design token in the shared requests-chart partial', function () {
$partial = file_get_contents(base_path('resources/views/livewire/traffic/_requests-chart.blade.php'));
expect($partial)->not->toContain('statusColorsLight');
expect($partial)->not->toContain('statusColorsDark');
expect($partial)->toContain('--chart-status-3xx');
});
it('keeps request values in a compact y-axis outside the plot', function () {
$partial = file_get_contents(base_path('resources/views/livewire/traffic/_requests-chart.blade.php'));
expect($partial)
->toContain('minWidth: 28')
->toContain('maxWidth: 28')
->toContain('padding: { left: 0, right: 12, top: 12, bottom: 0 }')
->not->toContain('floating: true')
->not->toContain('offsetX: 32');
});
it('renders request charts full bleed inside their analytics sections', function () {
foreach ([
base_path('resources/views/livewire/analytics.blade.php'),
base_path('resources/views/livewire/project/application/analytics.blade.php'),
] as $viewPath) {
$view = file_get_contents($viewPath);
expect($view)->toContain('id="analytics-requests-section" title="Requests" flush');
}
});
it('seeds the requests chart with the server-rendered series', function () {
$partial = file_get_contents(base_path('resources/views/livewire/traffic/_requests-chart.blade.php'));
expect($partial)
->toContain("'initialCategories' => array_column(\$series, 'bucket')")
->toContain("'initialRequests' => \$this->requestsSpark()")
->toContain('series: [{ name: \'Requests\', data: initialPoints }]');
});
it('waits for lazy Livewire chart elements before initializing ApexCharts', function () {
foreach ([
base_path('resources/views/livewire/traffic/_requests-chart.blade.php'),
base_path('resources/views/livewire/traffic/_device-chart.blade.php'),
] as $partialPath) {
$partial = file_get_contents($partialPath);
expect($partial)
->toContain('requestAnimationFrame(() => {')
->toContain('if (!el) { return; }');
}
});
it('treats an all-zero device series as no data', function () {
$partial = file_get_contents(base_path('resources/views/livewire/traffic/_device-chart.blade.php'));
expect($partial)
->toContain("\$hasDeviceData = array_sum(array_map('intval', \$series)) > 0")
->toContain('@if (! $hasDeviceData)');
});
it('keeps KPI sparklines axisless after live updates', function () {
$partial = file_get_contents(base_path('resources/views/livewire/traffic/_sparkline.blade.php'));
expect($partial)
->toContain('yaxis: { labels: { show: false }')
->toContain("xaxis: { type: 'datetime', labels: { show: false }, axisBorder: { show: false }, axisTicks: { show: false } }")
->toContain('grid: { padding: { left: 4, right: 4, top: 0, bottom: 0 } }');
});
it('shows values and UTC timestamps in analytics chart tooltips', function () {
$sparkline = file_get_contents(base_path('resources/views/livewire/traffic/_sparkline.blade.php'));
$requestsChart = file_get_contents(base_path('resources/views/livewire/traffic/_requests-chart.blade.php'));
expect($sparkline)
->toContain('sparkCategories')
->toContain('apexcharts-tooltip-custom-value')
->toContain('formatTimestamp(timestamp)');
expect($requestsChart)
->toContain('apexcharts-tooltip-custom-value')
->toContain('formatTimestamp(timestamp)');
foreach ([
app_path('Livewire/Analytics.php'),
app_path('Livewire/Project/Application/Analytics.php'),
app_path('Livewire/Dashboard/TrafficAnalytics.php'),
] as $component) {
expect(file_get_contents($component))->toContain("'sparkCategories' => array_column(\$this->series, 'bucket')");
}
});
it('registers sparkline refresh listeners without Blade control-flow inside Alpine data', function () {
$partial = file_get_contents(base_path('resources/views/livewire/traffic/_sparkline.blade.php'));
expect($partial)
->not->toContain('@if ($event && $key)')
->toContain('if (event && key)')
->toContain('this.refreshCleanup = Livewire.on(event')
->toContain('destroy()');
});
it('lets KPI hover markers overflow without shrinking the chart scale', function () {
$partial = file_get_contents(base_path('resources/views/livewire/traffic/_sparkline.blade.php'));
expect($partial)
->toContain('[&_.apexcharts-svg]:overflow-visible!')
->not->toContain('headroom')
->toContain('markers: { size: 0, hover: { size: 5, sizeOffset: 2 } }');
});
it('uses the standard page title styling in the analytics view and placeholder', function () {
foreach ([
base_path('resources/views/livewire/analytics.blade.php'),
base_path('resources/views/livewire/analytics-placeholder.blade.php'),
] as $view) {
$contents = file_get_contents($view);
expect($contents)
->toContain('<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">Analytics</h1>')
->not->toContain('<x-reicon name="analytics"');
}
});