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(); });