mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-14 00:33:44 -04:00
- Runtime logs: add a compact text view, move display options and log level filters into a View menu, move copy and download into an Export menu, and remove the extra top margin. - Replace spinner loading cards with skeletons on the logs, dynamic configurations, proxy, TLS certificates, Docker images, environment variables, preview deployments, rollback, subscription, cloud provider, and new resource pages.
185 lines
7.1 KiB
PHP
185 lines
7.1 KiB
PHP
<?php
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Process\PendingProcess;
|
|
use Illuminate\Support\Facades\Process;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config()->set('app.maintenance.store', 'array');
|
|
$this->stack = seedBrowserResourceStack();
|
|
|
|
// 50,000 timestamped lines, the maximum the viewer accepts.
|
|
$this->bigLog = implode("\n", array_map(
|
|
fn (int $i) => sprintf('2026-09-28T10:%02d:%02d.%09dZ log line %05d', intdiv($i, 3600) % 60, intdiv($i, 60) % 60, $i, $i),
|
|
range(1, 50000),
|
|
));
|
|
|
|
Process::fake(function (PendingProcess $process) {
|
|
if (str_contains($process->command, '--since')) {
|
|
return Process::result(output: '2026-09-28T23:59:59Z streamed line after since');
|
|
}
|
|
if (str_contains($process->command, 'docker logs')) {
|
|
return Process::result(output: $this->bigLog);
|
|
}
|
|
|
|
return Process::result();
|
|
});
|
|
});
|
|
|
|
it('renders 50,000 runtime log lines with only the visible rows in the DOM', function () {
|
|
$page = visit('/login')
|
|
->fill('email', 'test@example.com')
|
|
->fill('password', 'password')
|
|
->click('Login')
|
|
->assertSee('Dashboard')
|
|
->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs");
|
|
|
|
$page->assertSee('log line 00001');
|
|
|
|
expect($page->script('document.querySelectorAll("[data-log-line]").length'))->toBeLessThan(100);
|
|
|
|
// Scroll to the end: the last line is rendered, the first line is not.
|
|
$page->script('(() => { const el = document.getElementById("logsContainer"); el.scrollTop = el.scrollHeight; })()');
|
|
$page->wait(0.5)
|
|
->assertSee('log line 50000')
|
|
->assertDontSee('log line 00001');
|
|
|
|
expect($page->script('document.querySelectorAll("[data-log-line]").length'))->toBeLessThan(100);
|
|
|
|
$page->screenshot(filename: 'runtime-logs-virtualized-end');
|
|
});
|
|
|
|
it('searches all loaded lines and highlights matches', function () {
|
|
$page = visit('/login')
|
|
->fill('email', 'test@example.com')
|
|
->fill('password', 'password')
|
|
->click('Login')
|
|
->assertSee('Dashboard')
|
|
->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs");
|
|
|
|
$page->assertSee('log line 00001')
|
|
->type('input[aria-label="Find in logs"]', 'line 4999')
|
|
->wait(0.6)
|
|
->assertSee('log line 49990')
|
|
->assertDontSee('log line 00001');
|
|
|
|
expect($page->script('document.querySelectorAll(".log-highlight").length'))->toBe(10)
|
|
->and($page->script('document.querySelector(".log-highlight").textContent'))->toBe('line 4999');
|
|
|
|
// Row details still open inside the virtual list.
|
|
$page->click('log line 49990')
|
|
->wait(0.3);
|
|
|
|
expect($page->script('document.querySelectorAll(".runtime-log-detail").length'))->toBe(1)
|
|
->and($page->script('document.querySelector(".runtime-log-detail").textContent'))->toBe('log line 49990');
|
|
|
|
$page->screenshot(filename: 'runtime-logs-virtualized-search');
|
|
});
|
|
|
|
it('moves ctrl+f to the log search and lets a second ctrl+f reach the browser', function () {
|
|
$page = visit('/login')
|
|
->fill('email', 'test@example.com')
|
|
->fill('password', 'password')
|
|
->click('Login')
|
|
->assertSee('Dashboard')
|
|
->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs");
|
|
|
|
$page->assertSee('log line 00001')
|
|
->assertSee('Ctrl+F')
|
|
->script('document.activeElement.blur()');
|
|
|
|
$page->keys('body', 'Control+f');
|
|
|
|
expect($page->script('document.activeElement.getAttribute("aria-label")'))->toBe('Find in logs');
|
|
|
|
// In the search field, Ctrl+F is not intercepted, so the browser find can open.
|
|
expect($page->script('(() => {
|
|
const event = new KeyboardEvent("keydown", { key: "f", ctrlKey: true, bubbles: true, cancelable: true });
|
|
document.activeElement.dispatchEvent(event);
|
|
return event.defaultPrevented;
|
|
})()'))->toBeFalse();
|
|
|
|
$page->screenshot(filename: 'runtime-logs-find-shortcut');
|
|
});
|
|
|
|
it('streams only new lines and appends them to the loaded log', function () {
|
|
$page = visit('/login')
|
|
->fill('email', 'test@example.com')
|
|
->fill('password', 'password')
|
|
->click('Login')
|
|
->assertSee('Dashboard')
|
|
->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs");
|
|
|
|
$page->assertSee('log line 00001')
|
|
->click('[title="Stream Logs"]')
|
|
->wait(3)
|
|
->click('[title="Follow Logs"]')
|
|
->wait(0.5)
|
|
->assertSee('streamed line after since')
|
|
->assertSee('log line 50000');
|
|
|
|
Process::assertRan(fn ($process) => str_contains($process->command, 'docker logs --since 2026-09-28T10:13:53.000050000Z -t coolify-sentinel'));
|
|
|
|
$page->screenshot(filename: 'runtime-logs-virtualized-stream');
|
|
});
|
|
|
|
it('falls back to the default level filters when the saved filters are invalid', function () {
|
|
$page = visit('/login')
|
|
->fill('email', 'test@example.com')
|
|
->fill('password', 'password')
|
|
->click('Login')
|
|
->assertSee('Dashboard');
|
|
|
|
$page->script('localStorage.setItem("coolify-log-filters", "{")');
|
|
$page->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs")
|
|
->assertSee('log line 00001');
|
|
|
|
// A valid saved preference is still applied.
|
|
$page->script('localStorage.setItem("coolify-log-filters", JSON.stringify({ info: false }))');
|
|
$page->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs")
|
|
->wait(1)
|
|
->assertDontSee('log line 00001');
|
|
|
|
$page->screenshot(filename: 'runtime-logs-invalid-saved-filters');
|
|
});
|
|
|
|
it('shows compact text lines in text view and remembers the choice', function () {
|
|
$page = visit('/login')
|
|
->fill('email', 'test@example.com')
|
|
->fill('password', 'password')
|
|
->click('Login')
|
|
->assertSee('Dashboard')
|
|
->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs");
|
|
|
|
$page->assertSee('log line 00001')
|
|
->click('[title="View Options"]')
|
|
->click('button:visible:has-text("Text view")')
|
|
->wait(0.3);
|
|
|
|
$page->screenshot(filename: 'runtime-logs-view-menu');
|
|
|
|
expect($page->script('document.querySelector("[data-log-line]").getBoundingClientRect().height'))->toBeLessThan(24);
|
|
|
|
// Rows rendered before the switch are measured again, so no gaps stay between lines.
|
|
expect($page->script('(() => {
|
|
const rows = [...document.querySelectorAll(".runtime-log-virtual-row")].sort((a, b) => a.dataset.index - b.dataset.index);
|
|
return rows.slice(1).every((row, i) => row.getBoundingClientRect().top - rows[i].getBoundingClientRect().bottom < 1);
|
|
})()'))->toBeTrue();
|
|
|
|
// Rows do not open details in text view.
|
|
$page->click('[title="View Options"]')
|
|
->click('log line 00001')
|
|
->wait(0.3);
|
|
|
|
expect($page->script('document.querySelectorAll(".runtime-log-detail").length'))->toBe(0);
|
|
|
|
$page->screenshot(filename: 'runtime-logs-text-view');
|
|
|
|
$page->navigate("/server/{$this->stack['server']->uuid}/sentinel/logs")
|
|
->assertSee('log line 00001');
|
|
|
|
expect($page->script('document.getElementById("logs").classList.contains("runtime-log-text-view")'))->toBeTrue();
|
|
});
|