Files
coolify/resources/js/runtime-logs.test.js
T
Andras Bacsai 2e7cf8633b feat(logs): pick runtime log streams from service selectors
Replace the container accordion with Deployment and Service selectors
that show one stream, merging replica containers by timestamp. The
viewer fills the page, opens in text view, keeps whitespace, and only
lets grid rows expand when the detail shows more than the row.

Based on the proposal in discussion #12176 by PhilippMolitor.
2026-10-23 11:55:44 +02:00

148 lines
6.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
createTimestampFormatter,
filterLogLines,
getLogLevel,
highlightSegments,
isFindShortcut,
lastTimestamp,
newLinesSince,
parseLogLines,
prefersTextView,
isExpandableLog,
} from './runtime-logs.js';
const idGenerator = () => {
let id = 0;
return () => ++id;
};
test('text view is the default unless it was turned off', () => {
assert.equal(prefersTextView(null), true);
assert.equal(prefersTextView('true'), true);
assert.equal(prefersTextView('false'), false);
});
test('parses docker timestamped lines and skips blank lines', () => {
const lines = parseLogLines(
'2026-09-28T10:00:00.123456789Z Server started\n\n2026-09-28T10:00:01Z ERROR failed to connect\nplain line',
idGenerator(),
);
assert.deepEqual(lines.map(({ id, ts, text, level }) => ({ id, ts, text, level })), [
{ id: 1, ts: '2026-09-28T10:00:00.123456789Z', text: 'Server started', level: 'info' },
{ id: 2, ts: '2026-09-28T10:00:01Z', text: 'ERROR failed to connect', level: 'error' },
{ id: 3, ts: '', text: 'plain line', level: 'info' },
]);
assert.ok(Object.isFrozen(lines[0]));
});
test('detects log levels by keyword', () => {
assert.equal(getLogLevel('fatal: boom'), 'error');
assert.equal(getLogLevel('warning: disk'), 'warning');
assert.equal(getLogLevel('trace id=1'), 'debug');
assert.equal(getLogLevel('ready'), 'info');
});
test('filters by level and case-insensitive query', () => {
const lines = parseLogLines('ERROR Database down\nINFO Database up\nwarn cache miss', idGenerator());
assert.deepEqual(filterLogLines(lines, { error: true, warning: true, debug: true, info: true }, 'DATABASE').map((l) => l.text), [
'ERROR Database down',
'INFO Database up',
]);
assert.deepEqual(filterLogLines(lines, { error: false, warning: true, debug: true, info: true }, '').map((l) => l.text), [
'INFO Database up',
'warn cache miss',
]);
});
test('drops lines that a --since request returns again', () => {
const nextId = idGenerator();
const existing = parseLogLines(
'2026-09-28T10:00:00Z first\n2026-09-28T10:00:01Z same-second a\n2026-09-28T10:00:01Z same-second b',
nextId,
);
const incoming = parseLogLines(
'2026-09-28T10:00:01Z same-second a\n2026-09-28T10:00:01Z same-second b\n2026-09-28T10:00:01Z same-second c\n2026-09-28T10:00:02Z next',
nextId,
);
assert.equal(lastTimestamp(existing), '2026-09-28T10:00:01Z');
assert.deepEqual(newLinesSince(existing, incoming).map((l) => l.text), ['same-second c', 'next']);
});
test('keeps repeated identical lines that are new at the boundary', () => {
const nextId = idGenerator();
const existing = parseLogLines('2026-09-28T10:00:01Z ping', nextId);
const incoming = parseLogLines('2026-09-28T10:00:01Z ping\n2026-09-28T10:00:01Z ping', nextId);
assert.deepEqual(newLinesSince(existing, incoming).map((l) => l.text), ['ping']);
});
test('splits text into highlight segments without markup', () => {
assert.deepEqual(highlightSegments('<b>Error</b> error', 'error'), [
{ text: '<b>', match: false },
{ text: 'Error', match: true },
{ text: '</b> ', match: false },
{ text: 'error', match: true },
]);
assert.deepEqual(highlightSegments('plain', ' '), [{ text: 'plain', match: false }]);
});
test('formats timestamps in the server timezone and falls back to UTC', () => {
assert.equal(createTimestampFormatter('Europe/Budapest')('2025-12-04T11:48:39.136764033Z'), '2025-Dec-04 12:48:39');
assert.equal(createTimestampFormatter('Not/AZone')('2025-12-04T11:48:39Z'), '2025-Dec-04 11:48:39');
});
test('reads a source tag and searches it without changing the level', () => {
const lines = parseLogLines(
'Frankfurt\t2026-09-28T10:00:01Z hello from frankfurt\nHelsinki\t2026-09-28T10:00:02Z ERROR failed there\nplain line',
idGenerator(),
);
assert.deepEqual(lines.map(({ source, ts, text, level }) => ({ source, ts, text, level })), [
{ source: 'Frankfurt', ts: '2026-09-28T10:00:01Z', text: 'hello from frankfurt', level: 'info' },
{ source: 'Helsinki', ts: '2026-09-28T10:00:02Z', text: 'ERROR failed there', level: 'error' },
{ source: '', ts: '', text: 'plain line', level: 'info' },
]);
assert.deepEqual(
filterLogLines(lines, { error: true, warning: true, debug: true, info: true }, 'helsinki').map((line) => line.source),
['Helsinki'],
);
});
test('drops repeated lines per source and keeps timestamp order', () => {
const nextId = idGenerator();
const existing = parseLogLines(
'Frankfurt\t2026-09-28T10:00:01Z same\nHelsinki\t2026-09-28T10:00:01Z same',
nextId,
);
const incoming = parseLogLines(
'Frankfurt\t2026-09-28T10:00:01Z same\nHelsinki\t2026-09-28T10:00:02Z next\nFrankfurt\t2026-09-28T10:00:03Z later',
nextId,
);
assert.deepEqual(newLinesSince(existing, incoming).map((line) => [line.source, line.text]), [
['Helsinki', 'next'],
['Frankfurt', 'later'],
]);
});
test('detects Ctrl+F and Cmd+F only', () => {
assert.equal(isFindShortcut({ ctrlKey: true, key: 'f' }), true);
assert.equal(isFindShortcut({ metaKey: true, key: 'F' }), true);
assert.equal(isFindShortcut({ key: 'f' }), false);
assert.equal(isFindShortcut({ ctrlKey: true, shiftKey: true, key: 'F' }), false);
assert.equal(isFindShortcut({ ctrlKey: true, altKey: true, key: 'f' }), false);
assert.equal(isFindShortcut({ ctrlKey: true, key: 'g' }), false);
});
test('a row expands only when the detail shows more than the row', () => {
assert.equal(isExpandableLog('JOB QUEUE', false), false);
assert.equal(isExpandableLog('{ broken json', false), false);
assert.equal(isExpandableLog('a very long line', true), true);
assert.equal(isExpandableLog('{"level":"info","id":1}', false), true);
});