This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new dfec203070fd chore: add comprehensive test coverage for 
camel-jbang-plugin-tui (#24279)
dfec203070fd is described below

commit dfec203070fdfe0cddab5cebecfdd2c06f21afd0
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Jun 29 14:57:01 2026 +0200

    chore: add comprehensive test coverage for camel-jbang-plugin-tui (#24279)
    
    - Add 231 unit tests covering StatusParser, TuiHelper, SyntaxHighlighter, 
and all tab classes
    - Add 17 rendering tests (BorderRenderTest) verifying border characters in 
virtual terminal buffer
    - Add higher-level rendering tests for TUI tabs using 
Frame.forTesting(Buffer)
---
 .../dsl/jbang/core/commands/tui/ShellPanel.java    |   6 +-
 .../commands/tui/CamelMonitorParseKeyTest.java     | 182 ++++++++
 .../core/commands/tui/ErrorsTabRenderTest.java     | 288 ++++++++++++
 .../jbang/core/commands/tui/FuzzyFilterTest.java   | 185 ++++++++
 .../core/commands/tui/HealthTabRenderTest.java     | 330 ++++++++++++++
 .../dsl/jbang/core/commands/tui/LoadAvgTest.java   |  84 ++++
 .../commands/tui/MonitorContextRenderTest.java     | 218 ++++++++++
 .../core/commands/tui/MonitorContextTest.java      | 235 ++++++++++
 .../core/commands/tui/RoutesTabRenderTest.java     | 328 ++++++++++++++
 .../core/commands/tui/SearchHighlighterTest.java   | 233 ++++++++++
 .../jbang/core/commands/tui/ShellPanelTest.java    | 324 ++++++++++++++
 .../jbang/core/commands/tui/StatusParserTest.java  | 482 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/TuiHelperTest.java | 242 +++++++++++
 13 files changed, 3134 insertions(+), 3 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
index d92c7af8c92b..135548fc2eda 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanel.java
@@ -317,7 +317,7 @@ class ShellPanel {
         return lines;
     }
 
-    private static Line convertRow(long[] buffer, int offset, int width) {
+    static Line convertRow(long[] buffer, int offset, int width) {
         List<Span> spans = new ArrayList<>();
         int col = 0;
         while (col < width) {
@@ -488,7 +488,7 @@ class ShellPanel {
     //   Y: Bit 0=FG set, Bit 1=BG set, Bit 2=Dim, Bit 3=Italic
     //   F: Foreground r-g-b (3 hex nibbles)
     //   B: Background r-g-b (3 hex nibbles)
-    private static Style convertAttrToStyle(long attr) {
+    static Style convertAttrToStyle(long attr) {
         Style style = Style.EMPTY;
 
         int x = (int) ((attr >> 24) & 0xF);
@@ -571,7 +571,7 @@ class ShellPanel {
         };
     }
 
-    private static byte[] encodeKeyEvent(KeyEvent ke) {
+    static byte[] encodeKeyEvent(KeyEvent ke) {
         if (ke.code() == KeyCode.CHAR) {
             char ch = ke.character();
             if (ke.hasCtrl()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorParseKeyTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorParseKeyTest.java
new file mode 100644
index 000000000000..656ab3167490
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorParseKeyTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CamelMonitorParseKeyTest {
+
+    @Test
+    void parseKeySingleChar() {
+        KeyEvent ke = CamelMonitor.parseKey("a");
+        assertNotNull(ke);
+        assertEquals(KeyCode.CHAR, ke.code());
+        assertEquals('a', ke.character());
+    }
+
+    @Test
+    void parseKeyEnter() {
+        KeyEvent ke = CamelMonitor.parseKey("enter");
+        assertNotNull(ke);
+        assertEquals(KeyCode.ENTER, ke.code());
+    }
+
+    @Test
+    void parseKeyReturn() {
+        KeyEvent ke = CamelMonitor.parseKey("return");
+        assertNotNull(ke);
+        assertEquals(KeyCode.ENTER, ke.code());
+    }
+
+    @Test
+    void parseKeyEscape() {
+        KeyEvent ke = CamelMonitor.parseKey("esc");
+        assertNotNull(ke);
+        assertEquals(KeyCode.ESCAPE, ke.code());
+    }
+
+    @Test
+    void parseKeyEscapeFull() {
+        KeyEvent ke = CamelMonitor.parseKey("escape");
+        assertNotNull(ke);
+        assertEquals(KeyCode.ESCAPE, ke.code());
+    }
+
+    @Test
+    void parseKeyTab() {
+        KeyEvent ke = CamelMonitor.parseKey("tab");
+        assertNotNull(ke);
+        assertEquals(KeyCode.TAB, ke.code());
+    }
+
+    @Test
+    void parseKeyBackspace() {
+        KeyEvent ke = CamelMonitor.parseKey("backspace");
+        assertNotNull(ke);
+        assertEquals(KeyCode.BACKSPACE, ke.code());
+    }
+
+    @Test
+    void parseKeyDelete() {
+        KeyEvent ke = CamelMonitor.parseKey("delete");
+        assertNotNull(ke);
+        assertEquals(KeyCode.DELETE, ke.code());
+    }
+
+    @Test
+    void parseKeyDeleteShort() {
+        KeyEvent ke = CamelMonitor.parseKey("del");
+        assertNotNull(ke);
+        assertEquals(KeyCode.DELETE, ke.code());
+    }
+
+    @Test
+    void parseKeyArrows() {
+        assertEquals(KeyCode.UP, CamelMonitor.parseKey("up").code());
+        assertEquals(KeyCode.DOWN, CamelMonitor.parseKey("down").code());
+        assertEquals(KeyCode.LEFT, CamelMonitor.parseKey("left").code());
+        assertEquals(KeyCode.RIGHT, CamelMonitor.parseKey("right").code());
+    }
+
+    @Test
+    void parseKeyHomeEnd() {
+        assertEquals(KeyCode.HOME, CamelMonitor.parseKey("home").code());
+        assertEquals(KeyCode.END, CamelMonitor.parseKey("end").code());
+    }
+
+    @Test
+    void parseKeyPageUpDown() {
+        assertEquals(KeyCode.PAGE_UP, CamelMonitor.parseKey("pageup").code());
+        assertEquals(KeyCode.PAGE_UP, CamelMonitor.parseKey("pgup").code());
+        assertEquals(KeyCode.PAGE_DOWN, 
CamelMonitor.parseKey("pagedown").code());
+        assertEquals(KeyCode.PAGE_DOWN, CamelMonitor.parseKey("pgdn").code());
+    }
+
+    @Test
+    void parseKeyFKeys() {
+        assertEquals(KeyCode.F1, CamelMonitor.parseKey("f1").code());
+        assertEquals(KeyCode.F6, CamelMonitor.parseKey("f6").code());
+        assertEquals(KeyCode.F12, CamelMonitor.parseKey("f12").code());
+    }
+
+    @Test
+    void parseKeySpace() {
+        KeyEvent ke = CamelMonitor.parseKey("space");
+        assertNotNull(ke);
+        assertEquals(KeyCode.CHAR, ke.code());
+        assertEquals(' ', ke.character());
+    }
+
+    @Test
+    void parseKeyCtrlModifier() {
+        KeyEvent ke = CamelMonitor.parseKey("Ctrl+c");
+        assertNotNull(ke);
+        assertEquals(KeyCode.CHAR, ke.code());
+        assertEquals('c', ke.character());
+        assertTrue(ke.hasCtrl());
+    }
+
+    @Test
+    void parseKeyShiftModifier() {
+        KeyEvent ke = CamelMonitor.parseKey("Shift+F6");
+        assertNotNull(ke);
+        assertEquals(KeyCode.F6, ke.code());
+        assertTrue(ke.hasShift());
+    }
+
+    @Test
+    void parseKeyCtrlAndShift() {
+        KeyEvent ke = CamelMonitor.parseKey("Ctrl+Shift+a");
+        assertNotNull(ke);
+        assertTrue(ke.hasCtrl());
+        assertTrue(ke.hasShift());
+    }
+
+    @Test
+    void parseKeyNullReturnsNull() {
+        assertNull(CamelMonitor.parseKey(null));
+    }
+
+    @Test
+    void parseKeyEmptyReturnsNull() {
+        assertNull(CamelMonitor.parseKey(""));
+    }
+
+    @Test
+    void parseKeyCaseInsensitive() {
+        KeyEvent ke1 = CamelMonitor.parseKey("ENTER");
+        assertNotNull(ke1);
+        assertEquals(KeyCode.ENTER, ke1.code());
+
+        KeyEvent ke2 = CamelMonitor.parseKey("Enter");
+        assertNotNull(ke2);
+        assertEquals(KeyCode.ENTER, ke2.code());
+    }
+
+    @Test
+    void parseKeyUnknownMultiCharReturnsNull() {
+        // Multi-character string that is not a known key name
+        assertNull(CamelMonitor.parseKey("xyz"));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
new file mode 100644
index 000000000000..8305970033d5
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Higher-level rendering tests for {@link ErrorsTab}. Renders the errors 
table into a virtual terminal buffer and
+ * inspects the rendered output.
+ */
+class ErrorsTabRenderTest {
+
+    private MonitorContext ctx;
+    private IntegrationInfo info;
+
+    @BeforeEach
+    void setUp() {
+        info = new IntegrationInfo();
+        info.pid = "9999";
+        info.name = "error-test-app";
+
+        AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(List.of(info));
+        AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(List.of());
+        ctx = new MonitorContext(data, infraData);
+        ctx.selectedPid = "9999";
+    }
+
+    @Test
+    void renderErrorsShowsTableHeaders() {
+        addError("ID-001", "route1", "to1", "IOException", "Connection 
refused", false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 160, 30);
+
+        assertTrue(rendered.contains("ID"), "Should show ID header");
+        assertTrue(rendered.contains("ROUTE"), "Should show ROUTE header");
+        assertTrue(rendered.contains("NODE"), "Should show NODE header");
+        assertTrue(rendered.contains("HANDLED"), "Should show HANDLED header");
+        assertTrue(rendered.contains("EXCEPTION"), "Should show EXCEPTION 
header");
+        assertTrue(rendered.contains("MESSAGE"), "Should show MESSAGE header");
+    }
+
+    @Test
+    void renderErrorShowsExchangeId() {
+        addError("ID-myhost-1234", "route1", "to1", "IOException", "timeout", 
false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 160, 30);
+
+        assertTrue(rendered.contains("ID-myhost-1234"), "Should render the 
exchange ID");
+    }
+
+    @Test
+    void renderErrorShowsShortExceptionName() {
+        addError("ID-001", "route1", "to1", "java.io.IOException", "Connection 
refused", false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 160, 30);
+
+        assertTrue(rendered.contains("IOException"), "Should show short 
exception type name");
+    }
+
+    @Test
+    void renderErrorRouteIdInCyan() {
+        addError("ID-001", "my-route", "to1", "Exception", "fail", false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+
+        Rect area = new Rect(0, 0, 160, 30);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        boolean foundCyan = findCellWithColor(buffer, "m", Color.CYAN);
+        assertTrue(foundCyan, "Route ID should use CYAN color");
+    }
+
+    @Test
+    void renderHandledTrueUsesGreenColor() {
+        addError("ID-001", "route1", "to1", "Exception", "handled", true);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+
+        Rect area = new Rect(0, 0, 160, 30);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        boolean foundGreen = findCellWithColor(buffer, "t", Color.GREEN);
+        assertTrue(foundGreen, "handled=true should be rendered in GREEN");
+    }
+
+    @Test
+    void renderHandledFalseUsesRedColor() {
+        addError("ID-001", "route1", "to1", "Exception", "unhandled", false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+
+        Rect area = new Rect(0, 0, 160, 30);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        boolean foundRed = findCellWithColor(buffer, "f", Color.LIGHT_RED);
+        assertTrue(foundRed, "handled=false should be rendered in LIGHT_RED");
+    }
+
+    @Test
+    void renderMultipleErrorsAllAppear() {
+        addError("ID-AAA", "route-a", "to1", "IOException", "fail1", false);
+        addError("ID-BBB", "route-b", "to2", "NullPointerException", "fail2", 
true);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 160, 30);
+
+        assertTrue(rendered.contains("ID-AAA"), "Should render first error");
+        assertTrue(rendered.contains("ID-BBB"), "Should render second error");
+        assertTrue(rendered.contains("IOException"), "Should render first 
exception type");
+        assertTrue(rendered.contains("NullPointerException"), "Should render 
second exception type");
+    }
+
+    @Test
+    void renderEmptyErrorsShowsPlaceholder() {
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 160, 20);
+
+        assertTrue(rendered.contains("No errors captured"),
+                "Should show placeholder when no errors exist");
+    }
+
+    @Test
+    void renderShowsErrorCount() {
+        addError("ID-001", "r1", "n1", "Ex", "m1", false);
+        addError("ID-002", "r2", "n2", "Ex", "m2", false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 160, 30);
+
+        assertTrue(rendered.contains("Errors (2)"), "Title should show error 
count");
+    }
+
+    @Test
+    void renderNoSelectionShowsPrompt() {
+        ctx.selectedPid = null;
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 120, 15);
+
+        assertTrue(rendered.contains("No integration selected") || 
rendered.contains("Select an integration"),
+                "Should show selection prompt when no integration selected");
+    }
+
+    @Test
+    void renderShowsSortColumn() {
+        addError("ID-001", "r1", "n1", "Ex", "msg", false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 160, 30);
+
+        assertTrue(rendered.contains("sort:id"), "Title should show current 
sort column");
+    }
+
+    @Test
+    void sortCycleChangesSortIndicator() {
+        addError("ID-001", "r1", "n1", "Ex", "msg", false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+
+        // Press 's' to cycle sort
+        tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
+
+        String rendered = renderToString(tab, 160, 30);
+        assertTrue(rendered.contains("sort:age"), "Sort should cycle to 
'age'");
+    }
+
+    @Test
+    void handledFilterCycleFiltersErrors() {
+        addError("ID-001", "r1", "n1", "Ex", "unhandled", false);
+        addError("ID-002", "r2", "n2", "Ex", "handled", true);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+
+        // Default shows all
+        String all = renderToString(tab, 160, 30);
+        assertTrue(all.contains("ID-001"), "All filter should show unhandled 
errors");
+        assertTrue(all.contains("ID-002"), "All filter should show handled 
errors");
+
+        // Press 'f' to filter to handled=true
+        tab.handleKeyEvent(KeyEvent.ofChar('f', KeyModifiers.NONE));
+        String handledOnly = renderToString(tab, 160, 30);
+        assertTrue(handledOnly.contains("ID-002"), "handled filter should show 
handled error");
+        assertFalse(handledOnly.contains("ID-001"), "handled filter should 
hide unhandled error");
+    }
+
+    @Test
+    void renderFooterHintsContainExpectedKeys() {
+        ErrorsTab tab = new ErrorsTab(ctx);
+        List<Span> footerSpans = new ArrayList<>();
+        tab.renderFooter(footerSpans);
+
+        String footer = footerSpans.stream()
+                .map(Span::content)
+                .reduce("", String::concat);
+
+        assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
+        assertTrue(footer.contains("sort"), "Footer should contain sort hint");
+        assertTrue(footer.contains("handled"), "Footer should contain handled 
filter hint");
+        assertTrue(footer.contains("wrap"), "Footer should contain wrap hint");
+    }
+
+    @Test
+    void renderErrorMessageContent() {
+        addError("ID-001", "route1", "to1", "Exception", "Connection refused 
to host:8080", false);
+
+        ErrorsTab tab = new ErrorsTab(ctx);
+        String rendered = renderToString(tab, 160, 30);
+
+        assertTrue(rendered.contains("Connection refused"), "Should render the 
error message");
+    }
+
+    // ---- Helper methods ----
+
+    private void addError(
+            String exchangeId, String routeId, String nodeId,
+            String exceptionType, String message, boolean handled) {
+        ErrorInfo ei = new ErrorInfo();
+        ei.exchangeId = exchangeId;
+        ei.routeId = routeId;
+        ei.nodeId = nodeId;
+        ei.exceptionType = exceptionType;
+        ei.exceptionMessage = message;
+        ei.handled = handled;
+        ei.timestamp = System.currentTimeMillis();
+        info.errors.add(ei);
+    }
+
+    private static String renderToString(MonitorTab tab, int width, int 
height) {
+        Rect area = new Rect(0, 0, width, height);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+        return HealthTabRenderTest.bufferToString(buffer);
+    }
+
+    private static boolean findCellWithColor(Buffer buffer, String symbol, 
Color expectedFg) {
+        for (int y = 0; y < buffer.height(); y++) {
+            for (int x = 0; x < buffer.width(); x++) {
+                var cell = buffer.get(x, y);
+                if (symbol.equals(cell.symbol())) {
+                    var fg = cell.style().fg().orElse(null);
+                    if (expectedFg.equals(fg)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilterTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilterTest.java
new file mode 100644
index 000000000000..d75bcb1255f2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilterTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class FuzzyFilterTest {
+
+    // ---- fuzzyMatch tests ----
+
+    @Test
+    void fuzzyMatchExactMatch() {
+        int[] positions = FuzzyFilter.fuzzyMatch("hello", "hello");
+        assertNotNull(positions);
+        assertArrayEquals(new int[] { 0, 1, 2, 3, 4 }, positions);
+    }
+
+    @Test
+    void fuzzyMatchScatteredMatch() {
+        // Pattern "hlo" should match positions of 'h', 'l', 'o' in "hello"
+        int[] positions = FuzzyFilter.fuzzyMatch("hello", "hlo");
+        assertNotNull(positions);
+        assertArrayEquals(new int[] { 0, 2, 4 }, positions);
+    }
+
+    @Test
+    void fuzzyMatchNoMatch() {
+        int[] positions = FuzzyFilter.fuzzyMatch("hello", "xyz");
+        assertNull(positions);
+    }
+
+    @Test
+    void fuzzyMatchEmptyPattern() {
+        int[] positions = FuzzyFilter.fuzzyMatch("hello", "");
+        assertNotNull(positions);
+        assertEquals(0, positions.length);
+    }
+
+    @Test
+    void fuzzyMatchNullPattern() {
+        int[] positions = FuzzyFilter.fuzzyMatch("hello", null);
+        assertNotNull(positions);
+        assertEquals(0, positions.length);
+    }
+
+    @Test
+    void fuzzyMatchCaseInsensitive() {
+        int[] positions = FuzzyFilter.fuzzyMatch("Hello World", "hw");
+        assertNotNull(positions);
+        assertEquals(2, positions.length);
+        assertEquals(0, positions[0]); // 'H' matches 'h'
+        assertEquals(6, positions[1]); // 'W' matches 'w'
+    }
+
+    // ---- highlightLine tests ----
+
+    @Test
+    void highlightLineMatchAtStart() {
+        Style normal = Style.EMPTY;
+        Style match = Style.EMPTY.fg(Color.RED);
+
+        Line line = FuzzyFilter.highlightLine("ABC", new int[] { 0 }, normal, 
match);
+        List<Span> spans = line.spans();
+        assertEquals(2, spans.size());
+        assertEquals("A", spans.get(0).content());
+        assertEquals(match, spans.get(0).style());
+        assertEquals("BC", spans.get(1).content());
+        assertEquals(normal, spans.get(1).style());
+    }
+
+    @Test
+    void highlightLineMatchAtEnd() {
+        Style normal = Style.EMPTY;
+        Style match = Style.EMPTY.fg(Color.RED);
+
+        Line line = FuzzyFilter.highlightLine("ABC", new int[] { 2 }, normal, 
match);
+        List<Span> spans = line.spans();
+        assertEquals(2, spans.size());
+        assertEquals("AB", spans.get(0).content());
+        assertEquals("C", spans.get(1).content());
+        assertEquals(match, spans.get(1).style());
+    }
+
+    @Test
+    void highlightLineConsecutiveMatches() {
+        Style normal = Style.EMPTY;
+        Style match = Style.EMPTY.fg(Color.RED);
+
+        Line line = FuzzyFilter.highlightLine("ABC", new int[] { 0, 1, 2 }, 
normal, match);
+        List<Span> spans = line.spans();
+        // Each matched char is its own span
+        assertEquals(3, spans.size());
+        for (Span span : spans) {
+            assertEquals(match, span.style());
+        }
+    }
+
+    @Test
+    void highlightLineNullPositions() {
+        Style normal = Style.EMPTY;
+        Style match = Style.EMPTY.fg(Color.RED);
+
+        Line line = FuzzyFilter.highlightLine("hello", null, normal, match);
+        List<Span> spans = line.spans();
+        assertEquals(1, spans.size());
+        assertEquals("hello", spans.get(0).content());
+        assertEquals(normal, spans.get(0).style());
+    }
+
+    @Test
+    void highlightLineEmptyPositions() {
+        Style normal = Style.EMPTY;
+        Style match = Style.EMPTY.fg(Color.RED);
+
+        Line line = FuzzyFilter.highlightLine("hello", new int[0], normal, 
match);
+        List<Span> spans = line.spans();
+        assertEquals(1, spans.size());
+        assertEquals("hello", spans.get(0).content());
+    }
+
+    // ---- appendChar / deleteChar / clearFilter state management ----
+
+    @Test
+    void appendCharDeleteCharClearFilterRoundTrip() {
+        FuzzyFilter filter = new FuzzyFilter();
+        assertFalse(filter.hasFilter());
+        assertEquals("", filter.filter());
+
+        filter.appendChar('A');
+        filter.appendChar('B');
+        assertTrue(filter.hasFilter());
+        assertEquals("ab", filter.filter()); // appendChar lowercases
+
+        filter.deleteChar();
+        assertEquals("a", filter.filter());
+
+        filter.clearFilter();
+        assertFalse(filter.hasFilter());
+        assertEquals("", filter.filter());
+    }
+
+    @Test
+    void deleteCharOnEmptyFilterIsNoop() {
+        FuzzyFilter filter = new FuzzyFilter();
+        filter.deleteChar(); // should not throw
+        assertEquals("", filter.filter());
+    }
+
+    @Test
+    void matchDelegatesToFuzzyMatch() {
+        FuzzyFilter filter = new FuzzyFilter();
+        filter.appendChar('h');
+        filter.appendChar('l');
+        int[] positions = filter.match("hello");
+        assertNotNull(positions);
+        assertArrayEquals(new int[] { 0, 2 }, positions);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java
new file mode 100644
index 000000000000..00c007fce4f9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTabRenderTest.java
@@ -0,0 +1,330 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Higher-level rendering tests for {@link HealthTab}. These tests render the 
tab into a virtual terminal buffer via
+ * {@link Frame#forTesting(Buffer)} and inspect the rendered cell content.
+ */
+class HealthTabRenderTest {
+
+    private MonitorContext ctx;
+    private IntegrationInfo info;
+
+    @BeforeEach
+    void setUp() {
+        info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "test-app";
+
+        AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(List.of(info));
+        AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(List.of());
+        ctx = new MonitorContext(data, infraData);
+        ctx.selectedPid = "1234";
+    }
+
+    @Test
+    void renderHealthChecksShowsUpStatusInGreen() {
+        HealthCheckInfo hc = new HealthCheckInfo();
+        hc.group = "camel";
+        hc.name = "context";
+        hc.state = "UP";
+        hc.readiness = true;
+        hc.liveness = true;
+        info.healthChecks.add(hc);
+
+        HealthTab tab = new HealthTab(ctx);
+        String rendered = renderToString(tab, 100, 20);
+
+        assertTrue(rendered.contains("context"), "Should render health check 
name 'context'");
+        assertTrue(rendered.contains("UP"), "Should render UP status");
+        assertTrue(rendered.contains("camel"), "Should render group name");
+    }
+
+    @Test
+    void renderHealthChecksShowsDownStatusWithMessage() {
+        HealthCheckInfo hc = new HealthCheckInfo();
+        hc.group = "routes";
+        hc.name = "timer-route";
+        hc.state = "DOWN";
+        hc.readiness = true;
+        hc.message = "Route stopped";
+        info.healthChecks.add(hc);
+
+        HealthTab tab = new HealthTab(ctx);
+        String rendered = renderToString(tab, 120, 20);
+
+        assertTrue(rendered.contains("timer-route"), "Should render health 
check name");
+        assertTrue(rendered.contains("DOWN"), "Should render DOWN status");
+        assertTrue(rendered.contains("Route stopped"), "Should render failure 
message");
+    }
+
+    @Test
+    void renderDownStatusCellUsesRedColor() {
+        HealthCheckInfo hc = new HealthCheckInfo();
+        hc.group = "routes";
+        hc.name = "failing-route";
+        hc.state = "DOWN";
+        hc.readiness = true;
+        info.healthChecks.add(hc);
+
+        HealthTab tab = new HealthTab(ctx);
+
+        Rect area = new Rect(0, 0, 100, 20);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        // Find the DOWN text cell and verify it uses red foreground
+        boolean foundRedDown = false;
+        for (int y = 0; y < buffer.height(); y++) {
+            for (int x = 0; x < buffer.width(); x++) {
+                var cell = buffer.get(x, y);
+                if ("✖".equals(cell.symbol()) || "D".equals(cell.symbol())) {
+                    var fg = cell.style().fg().orElse(null);
+                    if (Color.LIGHT_RED.equals(fg)) {
+                        foundRedDown = true;
+                        break;
+                    }
+                }
+            }
+            if (foundRedDown) {
+                break;
+            }
+        }
+        assertTrue(foundRedDown, "DOWN status should be rendered in 
LIGHT_RED");
+    }
+
+    @Test
+    void renderUpStatusCellUsesGreenColor() {
+        HealthCheckInfo hc = new HealthCheckInfo();
+        hc.group = "camel";
+        hc.name = "ctx";
+        hc.state = "UP";
+        hc.readiness = true;
+        hc.liveness = true;
+        info.healthChecks.add(hc);
+
+        HealthTab tab = new HealthTab(ctx);
+
+        Rect area = new Rect(0, 0, 100, 20);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        // Find a green-colored cell in the UP row
+        boolean foundGreenUp = false;
+        for (int y = 0; y < buffer.height(); y++) {
+            for (int x = 0; x < buffer.width(); x++) {
+                var cell = buffer.get(x, y);
+                if ("✔".equals(cell.symbol())) {
+                    var fg = cell.style().fg().orElse(null);
+                    if (Color.GREEN.equals(fg)) {
+                        foundGreenUp = true;
+                        break;
+                    }
+                }
+            }
+            if (foundGreenUp) {
+                break;
+            }
+        }
+        assertTrue(foundGreenUp, "UP status should be rendered in GREEN");
+    }
+
+    @Test
+    void renderEmptyHealthChecksShowsPlaceholder() {
+        // No health checks added
+        HealthTab tab = new HealthTab(ctx);
+        // Use wider terminal to avoid column truncation of the placeholder 
text
+        String rendered = renderToString(tab, 140, 10);
+
+        assertTrue(rendered.contains("No health checks"),
+                "Should show placeholder when no health checks exist");
+    }
+
+    @Test
+    void renderMultipleHealthChecksAllAppear() {
+        addHealthCheck("camel", "context", "UP");
+        addHealthCheck("camel", "route-controller", "UP");
+        addHealthCheck("routes", "timer-route", "DOWN");
+
+        HealthTab tab = new HealthTab(ctx);
+        String rendered = renderToString(tab, 120, 20);
+
+        assertTrue(rendered.contains("context"), "Should render 'context' 
check");
+        assertTrue(rendered.contains("route-controller"), "Should render 
'route-controller' check");
+        assertTrue(rendered.contains("timer-route"), "Should render 
'timer-route' check");
+    }
+
+    @Test
+    void renderShowsHeaderColumns() {
+        addHealthCheck("camel", "context", "UP");
+
+        HealthTab tab = new HealthTab(ctx);
+        String rendered = renderToString(tab, 120, 20);
+
+        assertTrue(rendered.contains("GROUP"), "Should show GROUP header");
+        assertTrue(rendered.contains("NAME"), "Should show NAME header");
+        assertTrue(rendered.contains("STATUS"), "Should show STATUS header");
+        assertTrue(rendered.contains("KIND"), "Should show KIND header");
+        assertTrue(rendered.contains("MESSAGE"), "Should show MESSAGE header");
+    }
+
+    @Test
+    void renderShowsBorderWithTitle() {
+        addHealthCheck("camel", "context", "UP");
+
+        HealthTab tab = new HealthTab(ctx);
+        String rendered = renderToString(tab, 120, 20);
+
+        assertTrue(rendered.contains("Health"), "Should show 'Health' in the 
block title");
+    }
+
+    @Test
+    void renderNoSelectionShowsPrompt() {
+        ctx.selectedPid = null;
+
+        HealthTab tab = new HealthTab(ctx);
+        String rendered = renderToString(tab, 100, 10);
+
+        assertTrue(rendered.contains("No integration selected") || 
rendered.contains("Select an integration"),
+                "Should show selection prompt when no integration selected");
+    }
+
+    @Test
+    void renderReadinessLivenessKindColumn() {
+        HealthCheckInfo hc = new HealthCheckInfo();
+        hc.group = "camel";
+        hc.name = "context";
+        hc.state = "UP";
+        hc.readiness = true;
+        hc.liveness = true;
+        info.healthChecks.add(hc);
+
+        HealthTab tab = new HealthTab(ctx);
+        String rendered = renderToString(tab, 120, 20);
+
+        assertTrue(rendered.contains("R/L"), "Should show R/L for 
readiness+liveness");
+    }
+
+    @Test
+    void toggleDownOnlyFilterChangesTitle() {
+        addHealthCheck("camel", "context", "UP");
+        addHealthCheck("routes", "failing", "DOWN");
+
+        HealthTab tab = new HealthTab(ctx);
+
+        // Press 'd' to toggle DOWN-only filter
+        tab.handleKeyEvent(KeyEvent.ofChar('d', KeyModifiers.NONE));
+        assertTrue(tab.isShowOnlyDown());
+
+        String rendered = renderToString(tab, 120, 20);
+        assertTrue(rendered.contains("DOWN only"), "Title should indicate 
DOWN-only filter");
+
+        // "context" (UP) should be filtered out
+        assertFalse(rendered.contains("context"), "UP check should be filtered 
out");
+        assertTrue(rendered.contains("failing"), "DOWN check should still 
appear");
+    }
+
+    @Test
+    void renderFooterHintsContainExpectedKeys() {
+        HealthTab tab = new HealthTab(ctx);
+        List<Span> footerSpans = new ArrayList<>();
+        tab.renderFooter(footerSpans);
+
+        String footer = footerSpans.stream()
+                .map(Span::content)
+                .reduce("", String::concat);
+
+        assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
+        assertTrue(footer.contains("sort"), "Footer should contain sort hint");
+        assertTrue(footer.contains("DOWN"), "Footer should contain DOWN toggle 
hint");
+    }
+
+    @Test
+    void sortCycleChangesHeaderAppearance() {
+        addHealthCheck("camel", "context", "UP");
+
+        HealthTab tab = new HealthTab(ctx);
+
+        // Default sort is "name" — header should have a sort indicator on NAME
+        String rendered1 = renderToString(tab, 120, 20);
+        assertTrue(rendered1.contains("NAME▼") || rendered1.contains("NAME▲"),
+                "Default sort should show indicator on NAME column");
+
+        // Press 's' to cycle to next sort column
+        tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
+        String rendered2 = renderToString(tab, 120, 20);
+        assertTrue(rendered2.contains("STATUS▼") || 
rendered2.contains("STATUS▲"),
+                "After one sort cycle, indicator should move to STATUS");
+    }
+
+    // ---- Helper methods ----
+
+    private void addHealthCheck(String group, String name, String state) {
+        HealthCheckInfo hc = new HealthCheckInfo();
+        hc.group = group;
+        hc.name = name;
+        hc.state = state;
+        hc.readiness = true;
+        info.healthChecks.add(hc);
+    }
+
+    /**
+     * Renders a tab into a virtual buffer and extracts the full text content.
+     */
+    private static String renderToString(MonitorTab tab, int width, int 
height) {
+        Rect area = new Rect(0, 0, width, height);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+        return bufferToString(buffer);
+    }
+
+    /**
+     * Extracts all text from a buffer row by row.
+     */
+    static String bufferToString(Buffer buffer) {
+        StringBuilder sb = new StringBuilder();
+        for (int y = 0; y < buffer.height(); y++) {
+            for (int x = 0; x < buffer.width(); x++) {
+                String sym = buffer.get(x, y).symbol();
+                sb.append(sym.isEmpty() ? " " : sym);
+            }
+            sb.append('\n');
+        }
+        return sb.toString();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvgTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvgTest.java
new file mode 100644
index 000000000000..45781a7449b9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LoadAvgTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class LoadAvgTest {
+
+    @Test
+    void noDataReturnsDash() {
+        LoadAvg avg = new LoadAvg();
+        assertEquals("-", avg.format("%.2f/%.2f/%.2f"));
+    }
+
+    @Test
+    void firstUpdateInitializes() {
+        LoadAvg avg = new LoadAvg();
+        avg.update(10.0);
+        String result = avg.format("%.2f/%.2f/%.2f");
+        assertNotEquals("-", result);
+        // After first update, all three loads should equal the initial value
+        assertEquals("10.00/10.00/10.00", result);
+    }
+
+    @Test
+    void multipleUpdatesConvergeViaEwma() {
+        LoadAvg avg = new LoadAvg();
+        avg.update(100.0);
+        // After initial value, update with 0 multiple times
+        // The EWMA should decay toward 0, with load1 decaying fastest
+        for (int i = 0; i < 100; i++) {
+            avg.update(0.0);
+        }
+        String result = avg.format("%.2f/%.2f/%.2f");
+        String[] parts = result.split("/");
+        double load1 = Double.parseDouble(parts[0]);
+        double load5 = Double.parseDouble(parts[1]);
+        double load15 = Double.parseDouble(parts[2]);
+
+        // load1 should have decayed more than load5, which decayed more than 
load15
+        assertTrue(load1 < load5, "load1 should decay faster than load5");
+        assertTrue(load5 < load15, "load5 should decay faster than load15");
+        // All should be less than the original 100
+        assertTrue(load15 < 100.0);
+    }
+
+    @Test
+    void steadyStateConverges() {
+        LoadAvg avg = new LoadAvg();
+        // Feed a constant value; all loads should converge to that value
+        for (int i = 0; i < 1000; i++) {
+            avg.update(50.0);
+        }
+        String result = avg.format("%.1f/%.1f/%.1f");
+        assertEquals("50.0/50.0/50.0", result);
+    }
+
+    @Test
+    void formatStringIsUsed() {
+        LoadAvg avg = new LoadAvg();
+        avg.update(1.23456);
+        // Check that the format string controls the output format
+        String result = avg.format("%.1f %.1f %.1f");
+        assertEquals("1.2 1.2 1.2", result);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
new file mode 100644
index 000000000000..875b1fd43e93
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextRenderTest.java
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link MonitorContext} rendering helpers that write to the 
terminal buffer. These exercise the actual
+ * Frame/Buffer rendering pipeline to verify that text and styles appear 
correctly.
+ */
+class MonitorContextRenderTest {
+
+    @Test
+    void renderNoSelectionShowsPromptText() {
+        Rect area = new Rect(0, 0, 80, 10);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+
+        MonitorContext.renderNoSelection(frame, area);
+
+        String rendered = HealthTabRenderTest.bufferToString(buffer);
+        assertTrue(rendered.contains("No integration selected"),
+                "Should render 'No integration selected' in the block title");
+        assertTrue(rendered.contains("Select an integration"),
+                "Should render the selection prompt text");
+    }
+
+    @Test
+    void renderNoSelectionRendersContentInBlock() {
+        Rect area = new Rect(0, 0, 80, 10);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+
+        MonitorContext.renderNoSelection(frame, area);
+
+        String rendered = HealthTabRenderTest.bufferToString(buffer);
+        // The block title should appear in the buffer
+        assertTrue(rendered.contains("No integration selected"),
+                "Should render the block title");
+        // The prompt text should appear somewhere in the buffer
+        assertTrue(rendered.contains("Select an integration"),
+                "Should render the prompt text");
+    }
+
+    @Test
+    void renderNoSelectionPromptIsDimmed() {
+        Rect area = new Rect(0, 0, 80, 10);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+
+        MonitorContext.renderNoSelection(frame, area);
+
+        // Check that the "Select" text cell has DIM modifier
+        boolean foundDim = false;
+        for (int y = 0; y < buffer.height(); y++) {
+            for (int x = 0; x < buffer.width(); x++) {
+                var cell = buffer.get(x, y);
+                if ("S".equals(cell.symbol()) && 
cell.style().effectiveModifiers()
+                        .contains(dev.tamboui.style.Modifier.DIM)) {
+                    foundDim = true;
+                    break;
+                }
+            }
+            if (foundDim) {
+                break;
+            }
+        }
+        assertTrue(foundDim, "Prompt text should use DIM modifier");
+    }
+
+    @Test
+    void hintAddsKeyAndLabel() {
+        List<Span> spans = new ArrayList<>();
+        MonitorContext.hint(spans, "Esc", "back");
+
+        assertEquals(2, spans.size(), "hint should add exactly 2 spans");
+        assertTrue(spans.get(0).content().contains("Esc"), "First span should 
contain the key");
+        assertTrue(spans.get(1).content().contains("back"), "Second span 
should contain the label");
+    }
+
+    @Test
+    void hintKeyUsesYellowBoldStyle() {
+        List<Span> spans = new ArrayList<>();
+        MonitorContext.hint(spans, "s", "sort");
+
+        Span keySpan = spans.get(0);
+        assertTrue(keySpan.style().fg().isPresent(), "Key span should have a 
foreground color");
+        assertEquals(Color.YELLOW, keySpan.style().fg().get(), "Key span 
should be YELLOW");
+        
assertTrue(keySpan.style().effectiveModifiers().contains(dev.tamboui.style.Modifier.BOLD),
+                "Key span should be BOLD");
+    }
+
+    @Test
+    void hintLabelHasTrailingSpaces() {
+        List<Span> spans = new ArrayList<>();
+        MonitorContext.hint(spans, "x", "action");
+
+        Span labelSpan = spans.get(1);
+        assertTrue(labelSpan.content().endsWith("  "),
+                "hint label should end with trailing spaces for separation");
+    }
+
+    @Test
+    void hintLastDoesNotHaveTrailingSpaces() {
+        List<Span> spans = new ArrayList<>();
+        MonitorContext.hintLast(spans, "q", "quit");
+
+        Span labelSpan = spans.get(1);
+        assertEquals(" quit", labelSpan.content(),
+                "hintLast label should not have trailing spaces");
+    }
+
+    @Test
+    void rightCellRendersRightAligned() {
+        var cell = MonitorContext.rightCell("42", 8);
+        // rightCell uses String.format("%8s", "42") → "      42"
+        String content = extractCellContent(cell);
+        assertTrue(content.endsWith("42"), "Content should end with the 
value");
+        assertEquals(8, content.length(), "Content should be padded to width 
8");
+    }
+
+    @Test
+    void rightCellWithStyleAppliesStyle() {
+        var cell = MonitorContext.rightCell("5", 6, 
dev.tamboui.style.Style.EMPTY.fg(Color.LIGHT_RED));
+        // The cell should have styled content
+        String content = extractCellContent(cell);
+        assertTrue(content.contains("5"), "Should contain the value");
+    }
+
+    @Test
+    void centerCellCentersText() {
+        var cell = MonitorContext.centerCell("x", 6);
+        String content = extractCellContent(cell);
+        // "x" centered in width 6: "  x" (leftPad = (6-1)/2 = 2)
+        assertTrue(content.startsWith("  "), "Content should have leading 
padding");
+        assertTrue(content.contains("x"), "Content should contain the value");
+    }
+
+    @Test
+    void sortLabelShowsIndicatorForActiveColumn() {
+        String label = MonitorContext.sortLabel("NAME", "name", "name", false);
+        assertEquals("NAME▼", label, "Active sort column should have 
descending indicator");
+
+        String reversed = MonitorContext.sortLabel("NAME", "name", "name", 
true);
+        assertEquals("NAME▲", reversed, "Reversed sort should have ascending 
indicator");
+    }
+
+    @Test
+    void sortLabelNoIndicatorForInactiveColumn() {
+        String label = MonitorContext.sortLabel("NAME", "name", "status", 
false);
+        assertEquals("NAME", label, "Inactive column should have no 
indicator");
+    }
+
+    @Test
+    void sortStyleActiveColumnIsYellowBold() {
+        var style = MonitorContext.sortStyle("name", "name");
+        assertEquals(Color.YELLOW, style.fg().orElse(null), "Active sort 
column should be YELLOW");
+        
assertTrue(style.effectiveModifiers().contains(dev.tamboui.style.Modifier.BOLD),
+                "Active sort column should be BOLD");
+    }
+
+    @Test
+    void sortStyleInactiveColumnIsBoldOnly() {
+        var style = MonitorContext.sortStyle("name", "status");
+        assertTrue(style.fg().isEmpty() || 
!Color.YELLOW.equals(style.fg().get()),
+                "Inactive column should not be YELLOW");
+        
assertTrue(style.effectiveModifiers().contains(dev.tamboui.style.Modifier.BOLD),
+                "Inactive column should still be BOLD");
+    }
+
+    private static String extractCellContent(dev.tamboui.widgets.table.Cell 
cell) {
+        // Render the cell into a small buffer to extract its text
+        Rect area = new Rect(0, 0, 20, 1);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        // Render via a simple single-row table
+        var table = dev.tamboui.widgets.table.Table.builder()
+                .rows(List.of(dev.tamboui.widgets.table.Row.from(cell)))
+                .widths(dev.tamboui.layout.Constraint.fill())
+                .build();
+        frame.renderStatefulWidget(table, area, new 
dev.tamboui.widgets.table.TableState());
+        return rowText(buffer, 0);
+    }
+
+    private static String rowText(Buffer buffer, int row) {
+        StringBuilder sb = new StringBuilder();
+        for (int col = 0; col < buffer.width(); col++) {
+            String sym = buffer.get(col, row).symbol();
+            sb.append(sym.isEmpty() ? " " : sym);
+        }
+        return sb.toString().stripTrailing();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextTest.java
new file mode 100644
index 000000000000..58cbf0b67b4b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContextTest.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Modifier;
+import dev.tamboui.style.Style;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MonitorContextTest {
+
+    // ---- formatSinceLast tests ----
+
+    @Test
+    void formatSinceLastAllThreePresent() {
+        String result = MonitorContext.formatSinceLast("1s", "2s", "3s");
+        assertEquals("1s/2s/3s", result);
+    }
+
+    @Test
+    void formatSinceLastOnlyStarted() {
+        String result = MonitorContext.formatSinceLast("5s", null, null);
+        assertEquals("5s", result);
+    }
+
+    @Test
+    void formatSinceLastOnlyCompleted() {
+        String result = MonitorContext.formatSinceLast(null, "10s", null);
+        assertEquals("10s", result);
+    }
+
+    @Test
+    void formatSinceLastOnlyFailed() {
+        String result = MonitorContext.formatSinceLast(null, null, "7s");
+        assertEquals("7s", result);
+    }
+
+    @Test
+    void formatSinceLastNonePresent() {
+        String result = MonitorContext.formatSinceLast(null, null, null);
+        assertEquals("", result);
+    }
+
+    @Test
+    void formatSinceLastStartedAndFailed() {
+        String result = MonitorContext.formatSinceLast("1s", null, "3s");
+        assertEquals("1s/3s", result);
+    }
+
+    // ---- formatLoad tests ----
+
+    @Test
+    void formatLoadNonZeroValues() {
+        String result = MonitorContext.formatLoad("1.23", "4.56", "7.89");
+        assertEquals("1.23/4.56/7.89", result);
+    }
+
+    @Test
+    void formatLoadZeroCollapse() {
+        String result = MonitorContext.formatLoad("0.00", "0.00", "0.00");
+        assertEquals("0/0/0", result);
+    }
+
+    @Test
+    void formatLoadNullHandling() {
+        String result = MonitorContext.formatLoad(null, null, null);
+        assertEquals("0/0/0", result);
+    }
+
+    @Test
+    void formatLoadMixedZeroAndNonZero() {
+        String result = MonitorContext.formatLoad("1.50", "0.00", "0.75");
+        assertEquals("1.50/0/0.75", result);
+    }
+
+    // ---- formatMemory tests ----
+
+    @Test
+    void formatMemoryBothPositive() {
+        // 512MB used, 1GB max
+        String result = MonitorContext.formatMemory(536870912L, 1073741824L);
+        assertEquals("512M/1024M", result);
+    }
+
+    @Test
+    void formatMemoryMaxZeroShowsUsedOnly() {
+        // 1048576 bytes = 1M (1024 * 1024)
+        String result = MonitorContext.formatMemory(1048576L, 0L);
+        assertEquals("1M", result);
+    }
+
+    @Test
+    void formatMemoryUsedZeroReturnsEmpty() {
+        String result = MonitorContext.formatMemory(0L, 1048576L);
+        assertEquals("", result);
+    }
+
+    // ---- formatBytes tests ----
+
+    @Test
+    void formatBytesRange() {
+        assertEquals("500B", MonitorContext.formatBytes(500));
+    }
+
+    @Test
+    void formatBytesKilobytes() {
+        assertEquals("1K", MonitorContext.formatBytes(1024));
+        assertEquals("10K", MonitorContext.formatBytes(10240));
+    }
+
+    @Test
+    void formatBytesMegabytes() {
+        assertEquals("1M", MonitorContext.formatBytes(1048576));
+        assertEquals("100M", MonitorContext.formatBytes(104857600));
+    }
+
+    // ---- buildBar tests ----
+
+    @Test
+    void buildBarFull() {
+        String bar = MonitorContext.buildBar(100, 100, 10);
+        assertEquals(10, bar.length());
+    }
+
+    @Test
+    void buildBarPartial() {
+        String bar = MonitorContext.buildBar(50, 100, 10);
+        assertEquals(5, bar.length());
+    }
+
+    @Test
+    void buildBarZeroValue() {
+        String bar = MonitorContext.buildBar(0, 100, 10);
+        assertEquals("", bar);
+    }
+
+    @Test
+    void buildBarZeroMax() {
+        String bar = MonitorContext.buildBar(50, 0, 10);
+        assertEquals("", bar);
+    }
+
+    @Test
+    void buildBarSmallValueRoundsCorrectly() {
+        // 1/1000 * 10 rounds to 0, buildBar returns empty for zero-length
+        String bar = MonitorContext.buildBar(1, 1000, 10);
+        assertEquals("", bar);
+
+        // A value large enough to produce at least one block (>= 1/maxWidth 
ratio)
+        String bar2 = MonitorContext.buildBar(100, 1000, 10);
+        assertTrue(bar2.length() >= 1);
+    }
+
+    // ---- topTimeStyle tests ----
+
+    @Test
+    void topTimeStyleOver1000ms() {
+        Style style = MonitorContext.topTimeStyle(1000);
+        assertTrue(style.effectiveModifiers().contains(Modifier.BOLD));
+        assertEquals(Color.LIGHT_RED, style.fg().orElse(null));
+    }
+
+    @Test
+    void topTimeStyleOver100ms() {
+        Style style = MonitorContext.topTimeStyle(500);
+        assertEquals(Color.YELLOW, style.fg().orElse(null));
+    }
+
+    @Test
+    void topTimeStyleUnder100ms() {
+        Style style = MonitorContext.topTimeStyle(50);
+        assertEquals(Style.EMPTY, style);
+    }
+
+    // ---- topDeltaStyle tests ----
+
+    @Test
+    void topDeltaStylePositive() {
+        Style style = MonitorContext.topDeltaStyle(10);
+        assertEquals(Color.LIGHT_RED, style.fg().orElse(null));
+    }
+
+    @Test
+    void topDeltaStyleNegative() {
+        Style style = MonitorContext.topDeltaStyle(-5);
+        assertEquals(Color.GREEN, style.fg().orElse(null));
+    }
+
+    @Test
+    void topDeltaStyleZero() {
+        Style style = MonitorContext.topDeltaStyle(0);
+        assertEquals(Style.EMPTY, style);
+    }
+
+    // ---- compareStr tests ----
+
+    @Test
+    void compareStrBothNull() {
+        assertEquals(0, MonitorContext.compareStr(null, null));
+    }
+
+    @Test
+    void compareStrFirstNull() {
+        assertEquals(1, MonitorContext.compareStr(null, "b"));
+    }
+
+    @Test
+    void compareStrSecondNull() {
+        assertEquals(-1, MonitorContext.compareStr("a", null));
+    }
+
+    @Test
+    void compareStrCaseInsensitive() {
+        assertEquals(0, MonitorContext.compareStr("ABC", "abc"));
+        assertTrue(MonitorContext.compareStr("abc", "xyz") < 0);
+        assertTrue(MonitorContext.compareStr("xyz", "abc") > 0);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
new file mode 100644
index 000000000000..a17a9d000b80
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Higher-level rendering tests for {@link RoutesTab}. Renders the routes 
table into a virtual terminal buffer and
+ * inspects the rendered output (text content, colors, layout).
+ */
+class RoutesTabRenderTest {
+
+    private MonitorContext ctx;
+    private IntegrationInfo info;
+
+    @BeforeEach
+    void setUp() {
+        info = new IntegrationInfo();
+        info.pid = "5678";
+        info.name = "my-integration";
+
+        AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(List.of(info));
+        AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(List.of());
+        ctx = new MonitorContext(data, infraData);
+        ctx.selectedPid = "5678";
+    }
+
+    @Test
+    void renderShowsRouteTableHeaders() {
+        addRoute("route1", "timer://tick?period=1000", "Started");
+
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 140, 30);
+
+        assertTrue(rendered.contains("ROUTE"), "Should show ROUTE header");
+        assertTrue(rendered.contains("FROM"), "Should show FROM header");
+        assertTrue(rendered.contains("STATUS"), "Should show STATUS header");
+        assertTrue(rendered.contains("TOTAL"), "Should show TOTAL header");
+        assertTrue(rendered.contains("FAIL"), "Should show FAIL header");
+    }
+
+    @Test
+    void renderShowsRouteIdAndFrom() {
+        addRoute("timer-to-log", "timer://hello?period=2000", "Started");
+
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 140, 30);
+
+        assertTrue(rendered.contains("timer-to-log"), "Should render route 
ID");
+        assertTrue(rendered.contains("timer://hello"), "Should render FROM 
endpoint");
+    }
+
+    @Test
+    void renderStartedRouteUsesGreenColor() {
+        addRoute("my-route", "direct://start", "Started");
+
+        RoutesTab tab = new RoutesTab(ctx);
+
+        Rect area = new Rect(0, 0, 140, 30);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        boolean foundGreenStarted = findCellWithColorContaining(buffer, "S", 
Color.GREEN);
+        assertTrue(foundGreenStarted, "Started status should be rendered in 
GREEN");
+    }
+
+    @Test
+    void renderStoppedRouteUsesRedColor() {
+        addRoute("stopped-route", "seda://queue", "Stopped");
+
+        RoutesTab tab = new RoutesTab(ctx);
+
+        Rect area = new Rect(0, 0, 140, 30);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        boolean foundRedStopped = findCellWithColorContaining(buffer, "S", 
Color.LIGHT_RED);
+        assertTrue(foundRedStopped, "Stopped status should be rendered in 
LIGHT_RED");
+    }
+
+    @Test
+    void renderRouteIdUsesCyanColor() {
+        addRoute("my-cyan-route", "timer://tick", "Started");
+
+        RoutesTab tab = new RoutesTab(ctx);
+
+        Rect area = new Rect(0, 0, 140, 30);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        boolean foundCyanRouteId = findCellWithColorContaining(buffer, "m", 
Color.CYAN);
+        assertTrue(foundCyanRouteId, "Route ID should be rendered in CYAN");
+    }
+
+    @Test
+    void renderMultipleRoutesAllAppear() {
+        addRoute("route-alpha", "timer://a", "Started");
+        addRoute("route-beta", "seda://b", "Started");
+        addRoute("route-gamma", "direct://c", "Stopped");
+
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 140, 30);
+
+        assertTrue(rendered.contains("route-alpha"), "Should render 
route-alpha");
+        assertTrue(rendered.contains("route-beta"), "Should render 
route-beta");
+        assertTrue(rendered.contains("route-gamma"), "Should render 
route-gamma");
+    }
+
+    @Test
+    void renderFailedCountHighlightedInRed() {
+        RouteInfo route = addRoute("failing-route", "timer://fail", "Started");
+        route.total = 100;
+        route.failed = 5;
+
+        RoutesTab tab = new RoutesTab(ctx);
+
+        Rect area = new Rect(0, 0, 140, 30);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+
+        boolean foundRedFailed = findCellWithColorContaining(buffer, "5", 
Color.LIGHT_RED);
+        assertTrue(foundRedFailed, "Failed count should be rendered in 
LIGHT_RED");
+    }
+
+    @Test
+    void renderShowsProcessorsSection() {
+        RouteInfo route = addRoute("my-route", "timer://tick", "Started");
+        ProcessorInfo proc = new ProcessorInfo();
+        proc.id = "log1";
+        proc.processor = "log";
+        proc.level = 1;
+        proc.total = 50;
+        route.processors.add(proc);
+
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 140, 30);
+
+        assertTrue(rendered.contains("Processors"), "Should show Processors 
section");
+        assertTrue(rendered.contains("log"), "Should show processor type");
+    }
+
+    @Test
+    void renderNoSelectionShowsPrompt() {
+        ctx.selectedPid = null;
+
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 100, 20);
+
+        assertTrue(rendered.contains("No integration selected") || 
rendered.contains("Select an integration"),
+                "Should show selection prompt when no integration selected");
+    }
+
+    @Test
+    void renderNoRoutesShowsEmpty() {
+        // info has no routes
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 140, 30);
+
+        assertTrue(rendered.contains("No routes") || 
rendered.contains("Routes"),
+                "Should show Routes section even with no routes");
+    }
+
+    @Test
+    void toggleTopModeChangesHeaders() {
+        addRoute("route1", "timer://tick", "Started");
+
+        RoutesTab tab = new RoutesTab(ctx);
+
+        // Render in normal mode first
+        String normalRender = renderToString(tab, 140, 30);
+        assertTrue(normalRender.contains("AGE"), "Normal mode should show AGE 
header");
+
+        // Press 't' to toggle top mode
+        tab.handleKeyEvent(KeyEvent.ofChar('t', KeyModifiers.NONE));
+        assertTrue(tab.isTopMode(), "Should be in top mode after pressing 
't'");
+
+        String topRender = renderToString(tab, 140, 30);
+        assertTrue(topRender.contains("MEAN"), "Top mode should show MEAN 
header");
+        assertTrue(topRender.contains("MAX"), "Top mode should show MAX 
header");
+        assertTrue(topRender.contains("LOAD"), "Top mode should show LOAD 
header");
+    }
+
+    @Test
+    void renderTopModeShowsTimingData() {
+        RouteInfo route = addRoute("route1", "timer://tick", "Started");
+        route.total = 1000;
+        route.meanTime = 42;
+        route.maxTime = 500;
+        route.minTime = 1;
+        route.lastTime = 35;
+
+        RoutesTab tab = new RoutesTab(ctx);
+        tab.handleKeyEvent(KeyEvent.ofChar('t', KeyModifiers.NONE));
+
+        String rendered = renderToString(tab, 140, 30);
+        assertTrue(rendered.contains("42"), "Should show mean time");
+        assertTrue(rendered.contains("500"), "Should show max time");
+    }
+
+    @Test
+    void renderShowsTitleWithSortColumn() {
+        addRoute("route1", "timer://tick", "Started");
+
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 140, 30);
+
+        assertTrue(rendered.contains("sort:name"), "Title should show current 
sort column");
+    }
+
+    @Test
+    void sortCycleChangesSortIndicator() {
+        addRoute("route1", "timer://tick", "Started");
+
+        RoutesTab tab = new RoutesTab(ctx);
+
+        // Press 's' to cycle sort
+        tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
+        String rendered = renderToString(tab, 140, 30);
+        assertTrue(rendered.contains("sort:from"), "Sort should cycle to 
'from'");
+    }
+
+    @Test
+    void renderFooterHintsContainExpectedKeys() {
+        RoutesTab tab = new RoutesTab(ctx);
+        List<Span> footerSpans = new ArrayList<>();
+        tab.renderFooter(footerSpans);
+
+        String footer = footerSpans.stream()
+                .map(Span::content)
+                .reduce("", String::concat);
+
+        assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
+        assertTrue(footer.contains("sort"), "Footer should contain sort hint");
+        assertTrue(footer.contains("topology"), "Footer should contain 
topology hint");
+    }
+
+    @Test
+    void renderRouteWithCoverageShowsCoverage() {
+        RouteInfo route = addRoute("route1", "timer://tick", "Started");
+        route.coverage = "5/5";
+
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 140, 30);
+
+        assertTrue(rendered.contains("5/5"), "Should show coverage value");
+    }
+
+    @Test
+    void renderRouteWithThroughputShowsValue() {
+        RouteInfo route = addRoute("route1", "timer://tick", "Started");
+        route.throughput = "1.50";
+
+        RoutesTab tab = new RoutesTab(ctx);
+        String rendered = renderToString(tab, 140, 30);
+
+        assertTrue(rendered.contains("1.50"), "Should show throughput value");
+    }
+
+    // ---- Helper methods ----
+
+    private RouteInfo addRoute(String routeId, String from, String state) {
+        RouteInfo route = new RouteInfo();
+        route.routeId = routeId;
+        route.from = from;
+        route.state = state;
+        route.uptime = "10s";
+        info.routes.add(route);
+        return route;
+    }
+
+    private static String renderToString(MonitorTab tab, int width, int 
height) {
+        Rect area = new Rect(0, 0, width, height);
+        Buffer buffer = Buffer.empty(area);
+        Frame frame = Frame.forTesting(buffer);
+        tab.render(frame, area);
+        return HealthTabRenderTest.bufferToString(buffer);
+    }
+
+    /**
+     * Search the buffer for any cell containing the given symbol text 
rendered with the specified foreground color.
+     */
+    private static boolean findCellWithColorContaining(Buffer buffer, String 
symbol, Color expectedFg) {
+        for (int y = 0; y < buffer.height(); y++) {
+            for (int x = 0; x < buffer.width(); x++) {
+                var cell = buffer.get(x, y);
+                if (symbol.equals(cell.symbol())) {
+                    var fg = cell.style().fg().orElse(null);
+                    if (expectedFg.equals(fg)) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighterTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighterTest.java
new file mode 100644
index 000000000000..5459b7fec9de
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SearchHighlighterTest.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.Arrays;
+import java.util.List;
+
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SearchHighlighterTest {
+
+    // ---- applyHighlights tests ----
+
+    @Test
+    void applyHighlightsNoMatchReturnsOriginal() {
+        SearchHighlighter sh = new SearchHighlighter();
+        Line original = Line.from(Span.styled("no match here", Style.EMPTY));
+
+        // No find or highlight patterns set, so should return original 
unchanged
+        Line result = sh.applyHighlights(original, 0, -1);
+        assertEquals(original.rawContent(), result.rawContent());
+        assertEquals(original.spans().size(), result.spans().size());
+    }
+
+    @Test
+    void applyHighlightsEmptyLineReturnsOriginal() {
+        SearchHighlighter sh = new SearchHighlighter();
+        Line empty = Line.from(Span.raw(""));
+
+        Line result = sh.applyHighlights(empty, 0, -1);
+        assertEquals("", result.rawContent());
+    }
+
+    @Test
+    void applyHighlightsWithHighlightTerm() {
+        SearchHighlighter sh = new SearchHighlighter();
+        // Activate highlight mode and set a term
+        sh.handleKeyEvent(KeyEvent.ofChar('h', KeyModifiers.NONE)); // 
activate highlight input
+        // Type "err"
+        sh.handleKeyEvent(KeyEvent.ofChar('e', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('r', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('r', KeyModifiers.NONE));
+        // Confirm with Enter
+        sh.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertTrue(sh.hasHighlightTerm());
+
+        Line line = Line.from(Span.styled("An error occurred", Style.EMPTY));
+        Line result = sh.applyHighlights(line, 0, -1);
+
+        // The word "err" should be highlighted
+        boolean foundHighlight = false;
+        for (Span span : result.spans()) {
+            if (span.content().equals("err")) {
+                assertEquals(SearchHighlighter.HIGHLIGHT_STYLE, span.style());
+                foundHighlight = true;
+            }
+        }
+        assertTrue(foundHighlight, "Expected 'err' to be highlighted");
+    }
+
+    @Test
+    void applyHighlightsWithFindCurrentLine() {
+        SearchHighlighter sh = new SearchHighlighter();
+        // Activate find mode
+        sh.handleKeyEvent(KeyEvent.ofChar('/', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('f', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('o', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('o', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertTrue(sh.hasFindTerm());
+
+        Line line = Line.from(Span.styled("foo bar foo", Style.EMPTY));
+        // lineIndex = 0, currentMatchLine = 0 means this IS the current match 
line
+        Line result = sh.applyHighlights(line, 0, 0);
+
+        // "foo" occurrences should use FIND_CURRENT_STYLE
+        boolean foundCurrent = false;
+        for (Span span : result.spans()) {
+            if (span.content().equals("foo")) {
+                assertEquals(SearchHighlighter.FIND_CURRENT_STYLE, 
span.style());
+                foundCurrent = true;
+            }
+        }
+        assertTrue(foundCurrent, "Expected 'foo' to use FIND_CURRENT_STYLE on 
current match line");
+    }
+
+    @Test
+    void applyHighlightsWithFindNonCurrentLine() {
+        SearchHighlighter sh = new SearchHighlighter();
+        // Activate find mode
+        sh.handleKeyEvent(KeyEvent.ofChar('/', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('b', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('a', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('r', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        Line line = Line.from(Span.styled("foo bar baz", Style.EMPTY));
+        // lineIndex = 5, currentMatchLine = 0 → NOT the current line
+        Line result = sh.applyHighlights(line, 5, 0);
+
+        boolean foundMatch = false;
+        for (Span span : result.spans()) {
+            if (span.content().equals("bar")) {
+                assertEquals(SearchHighlighter.FIND_MATCH_STYLE, span.style());
+                foundMatch = true;
+            }
+        }
+        assertTrue(foundMatch, "Expected 'bar' to use FIND_MATCH_STYLE on 
non-current line");
+    }
+
+    // ---- buildFindMatches + navigation tests ----
+
+    @Test
+    void buildFindMatchesFindsMatchingLines() {
+        SearchHighlighter sh = new SearchHighlighter();
+        sh.handleKeyEvent(KeyEvent.ofChar('/', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('e', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('r', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('r', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        List<String> lines = Arrays.asList(
+                "line one",
+                "error here",
+                "line three",
+                "another error");
+
+        sh.buildFindMatches(lines);
+        int first = sh.jumpToNearestMatch(0);
+        assertEquals(1, first); // line index 1 has "error"
+
+        sh.navigateToNextMatch();
+        assertEquals(3, sh.currentMatchLine()); // line index 3 has "error"
+
+        // Wrap around
+        sh.navigateToNextMatch();
+        assertEquals(1, sh.currentMatchLine());
+    }
+
+    @Test
+    void navigatePrevMatchWrapsBackward() {
+        SearchHighlighter sh = new SearchHighlighter();
+        sh.handleKeyEvent(KeyEvent.ofChar('/', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('x', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        List<String> lines = Arrays.asList("x1", "y2", "x3");
+        sh.buildFindMatches(lines);
+        sh.jumpToNearestMatch(0);
+
+        // At match 0 (line 0), going prev should wrap to last match
+        sh.navigateToPrevMatch();
+        assertEquals(2, sh.currentMatchLine()); // last match
+    }
+
+    @Test
+    void jumpToNearestMatchJumpsForward() {
+        SearchHighlighter sh = new SearchHighlighter();
+        sh.handleKeyEvent(KeyEvent.ofChar('/', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('z', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        List<String> lines = Arrays.asList("aaa", "bbb", "zzz", "ddd");
+        sh.buildFindMatches(lines);
+        int nearest = sh.jumpToNearestMatch(1);
+        assertEquals(2, nearest); // Jump to the nearest match at or after 
position 1
+    }
+
+    @Test
+    void noFindPatternReturnsEmptyMatches() {
+        SearchHighlighter sh = new SearchHighlighter();
+        sh.buildFindMatches(Arrays.asList("a", "b", "c"));
+        assertEquals(-1, sh.currentMatchLine());
+    }
+
+    // ---- handleEscape tests ----
+
+    @Test
+    void handleEscapeClearsFindTerm() {
+        SearchHighlighter sh = new SearchHighlighter();
+        sh.handleKeyEvent(KeyEvent.ofChar('/', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofChar('x', KeyModifiers.NONE));
+        sh.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        assertTrue(sh.hasFindTerm());
+
+        boolean handled = sh.handleEscape();
+        assertTrue(handled);
+        assertFalse(sh.hasFindTerm());
+    }
+
+    @Test
+    void handleEscapeDuringInputCancelsInput() {
+        SearchHighlighter sh = new SearchHighlighter();
+        sh.handleKeyEvent(KeyEvent.ofChar('/', KeyModifiers.NONE));
+        assertTrue(sh.isSearchInputActive());
+
+        boolean handled = sh.handleEscape();
+        assertTrue(handled);
+        assertFalse(sh.isSearchInputActive());
+    }
+
+    @Test
+    void handleEscapeNoActiveSearchReturnsFalse() {
+        SearchHighlighter sh = new SearchHighlighter();
+        assertFalse(sh.handleEscape());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
new file mode 100644
index 000000000000..18f5c5ab0cc9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ShellPanelTest.java
@@ -0,0 +1,324 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import dev.tamboui.style.Modifier;
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ShellPanelTest {
+
+    // ---- convertRow tests ----
+
+    @Test
+    void convertRowPlainAscii() {
+        // Build a buffer with plain ASCII chars and zeroed attributes
+        long[] buffer = new long[3];
+        buffer[0] = 'H';
+        buffer[1] = 'i';
+        buffer[2] = '!';
+
+        Line line = ShellPanel.convertRow(buffer, 0, 3);
+        assertEquals("Hi!", rawContent(line));
+    }
+
+    @Test
+    void convertRowNullCodepointBecomesSpace() {
+        // A zero codepoint (null cell) should be rendered as a space
+        long[] buffer = new long[3];
+        buffer[0] = 'A';
+        buffer[1] = 0; // null codepoint
+        buffer[2] = 'B';
+
+        Line line = ShellPanel.convertRow(buffer, 0, 3);
+        assertEquals("A B", rawContent(line));
+    }
+
+    @Test
+    void convertRowBoldAttr() {
+        // Bold is bit 3 of X nibble → set bit 27 (attr bit 3 in X at position 
24+3=27)
+        long attr = 0x8L << 24; // Bold
+        long[] buffer = new long[] { 'B' | (attr << 32) };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 1);
+        List<Span> spans = line.spans();
+        assertEquals(1, spans.size());
+        
assertTrue(spans.get(0).style().effectiveModifiers().contains(Modifier.BOLD));
+    }
+
+    @Test
+    void convertRowUnderlineAttr() {
+        // Underline is bit 0 of X nibble → bit 24
+        long attr = 0x1L << 24; // Underline
+        long[] buffer = new long[] { 'U' | (attr << 32) };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 1);
+        List<Span> spans = line.spans();
+        assertEquals(1, spans.size());
+        
assertTrue(spans.get(0).style().effectiveModifiers().contains(Modifier.UNDERLINED));
+    }
+
+    @Test
+    void convertRowFgColor() {
+        // FG set flag: bit 0 of Y (bit 28), FG color in bits 12-23
+        // FG = 0xF00 → red=0xF, green=0x0, blue=0x0
+        // Y=0x1 (FG set), X=0x0, FFF=0xF00, BBB=0x000
+        // attr = 0x10F00000L
+        long attr = 0x10F00000L;
+        long[] buffer = new long[] { 'R' | (attr << 32) };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 1);
+        List<Span> spans = line.spans();
+        assertEquals(1, spans.size());
+        assertTrue(spans.get(0).style().fg().isPresent());
+    }
+
+    @Test
+    void convertRowBgColor() {
+        // BG set flag: bit 1 of Y (bit 29), BG color in bits 0-11
+        // Y=0x2 (BG set), BBB=0x080 (green)
+        long attr = 0x20000080L;
+        long[] buffer = new long[] { 'G' | (attr << 32) };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 1);
+        List<Span> spans = line.spans();
+        assertEquals(1, spans.size());
+        assertTrue(spans.get(0).style().bg().isPresent());
+    }
+
+    @Test
+    void convertRowMergesContiguousSameAttr() {
+        // Two cells with the same attribute should be merged into one span
+        long attr = 0x8L << 24; // Bold
+        long[] buffer = new long[] {
+                'A' | (attr << 32),
+                'B' | (attr << 32)
+        };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 2);
+        List<Span> spans = line.spans();
+        assertEquals(1, spans.size());
+        assertEquals("AB", spans.get(0).content());
+    }
+
+    @Test
+    void convertRowSplitsDifferentAttrs() {
+        // Two cells with different attributes should become two spans
+        long boldAttr = 0x8L << 24;
+        long ulAttr = 0x1L << 24;
+        long[] buffer = new long[] {
+                'A' | (boldAttr << 32),
+                'B' | (ulAttr << 32)
+        };
+
+        Line line = ShellPanel.convertRow(buffer, 0, 2);
+        List<Span> spans = line.spans();
+        assertEquals(2, spans.size());
+        assertEquals("A", spans.get(0).content());
+        assertEquals("B", spans.get(1).content());
+    }
+
+    @Test
+    void convertRowWithOffset() {
+        // Test that offset is correctly used to read from the middle of the 
buffer
+        long[] buffer = new long[] { 'X', 'Y', 'A', 'B', 'C' };
+
+        Line line = ShellPanel.convertRow(buffer, 2, 3);
+        assertEquals("ABC", rawContent(line));
+    }
+
+    // ---- encodeKeyEvent tests ----
+
+    @Test
+    void encodeEnter() {
+        KeyEvent ke = KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE);
+        byte[] result = ShellPanel.encodeKeyEvent(ke);
+        assertArrayEquals(new byte[] { '\r' }, result);
+    }
+
+    @Test
+    void encodeBackspace() {
+        KeyEvent ke = KeyEvent.ofKey(KeyCode.BACKSPACE, KeyModifiers.NONE);
+        byte[] result = ShellPanel.encodeKeyEvent(ke);
+        assertArrayEquals(new byte[] { 0x7f }, result);
+    }
+
+    @Test
+    void encodeTab() {
+        KeyEvent ke = KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE);
+        byte[] result = ShellPanel.encodeKeyEvent(ke);
+        assertArrayEquals(new byte[] { '\t' }, result);
+    }
+
+    @Test
+    void encodeArrowKeys() {
+        assertArrayEquals("\033OA".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.UP, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033OB".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033OC".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.RIGHT, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033OD".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.LEFT, 
KeyModifiers.NONE)));
+    }
+
+    @Test
+    void encodeHomeEnd() {
+        assertArrayEquals("\033OH".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.HOME, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033OF".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.END, 
KeyModifiers.NONE)));
+    }
+
+    @Test
+    void encodeFKeys() {
+        assertArrayEquals("\033OP".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.F1, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033OQ".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.F2, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033[15~".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.F5, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033[24~".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.F12, 
KeyModifiers.NONE)));
+    }
+
+    @Test
+    void encodeCtrlLetter() {
+        // Ctrl+c should produce byte 3 (0x03)
+        KeyEvent ke = KeyEvent.ofChar('c', KeyModifiers.of(true, false, 
false));
+        byte[] result = ShellPanel.encodeKeyEvent(ke);
+        assertArrayEquals(new byte[] { 3 }, result);
+    }
+
+    @Test
+    void encodeCtrlUpperCaseLetter() {
+        // Ctrl+A should also produce byte 1
+        KeyEvent ke = KeyEvent.ofChar('A', KeyModifiers.of(true, false, 
false));
+        byte[] result = ShellPanel.encodeKeyEvent(ke);
+        assertArrayEquals(new byte[] { 1 }, result);
+    }
+
+    @Test
+    void encodeRegularChar() {
+        KeyEvent ke = KeyEvent.ofChar('x', KeyModifiers.NONE);
+        byte[] result = ShellPanel.encodeKeyEvent(ke);
+        assertArrayEquals("x".getBytes(StandardCharsets.UTF_8), result);
+    }
+
+    @Test
+    void encodePageUpDown() {
+        assertArrayEquals("\033[5~".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.PAGE_UP, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033[6~".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.PAGE_DOWN, 
KeyModifiers.NONE)));
+    }
+
+    @Test
+    void encodeInsertDelete() {
+        assertArrayEquals("\033[2~".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.INSERT, 
KeyModifiers.NONE)));
+        assertArrayEquals("\033[3~".getBytes(StandardCharsets.UTF_8),
+                ShellPanel.encodeKeyEvent(KeyEvent.ofKey(KeyCode.DELETE, 
KeyModifiers.NONE)));
+    }
+
+    @Test
+    void encodeUnhandledKeyReturnsNull() {
+        // F11 is not handled by encodeKeyEvent
+        KeyEvent ke = KeyEvent.ofKey(KeyCode.F11, KeyModifiers.NONE);
+        byte[] result = ShellPanel.encodeKeyEvent(ke);
+        assertNull(result);
+    }
+
+    // ---- convertAttrToStyle tests ----
+
+    @Test
+    void convertAttrToStyleNoFlags() {
+        Style style = ShellPanel.convertAttrToStyle(0);
+        assertTrue(style.effectiveModifiers().isEmpty());
+    }
+
+    @Test
+    void convertAttrToStyleBold() {
+        // Bold = bit 3 of X nibble (bits 24-27)
+        long attr = 0x08000000L;
+        Style style = ShellPanel.convertAttrToStyle(attr);
+        assertTrue(style.effectiveModifiers().contains(Modifier.BOLD));
+    }
+
+    @Test
+    void convertAttrToStyleUnderline() {
+        long attr = 0x01000000L;
+        Style style = ShellPanel.convertAttrToStyle(attr);
+        assertTrue(style.effectiveModifiers().contains(Modifier.UNDERLINED));
+    }
+
+    @Test
+    void convertAttrToStyleReversed() {
+        long attr = 0x02000000L;
+        Style style = ShellPanel.convertAttrToStyle(attr);
+        assertTrue(style.effectiveModifiers().contains(Modifier.REVERSED));
+    }
+
+    @Test
+    void convertAttrToStyleDim() {
+        // Dim = bit 2 of Y nibble (bits 28-31) → 0x4 << 28
+        long attr = 0x40000000L;
+        Style style = ShellPanel.convertAttrToStyle(attr);
+        assertTrue(style.effectiveModifiers().contains(Modifier.DIM));
+    }
+
+    @Test
+    void convertAttrToStyleItalic() {
+        // Italic = bit 3 of Y nibble → 0x8 << 28
+        long attr = 0x80000000L;
+        Style style = ShellPanel.convertAttrToStyle(attr);
+        assertTrue(style.effectiveModifiers().contains(Modifier.ITALIC));
+    }
+
+    @Test
+    void convertAttrToStyleCombinedFgBgBoldItalic() {
+        // Y = 0xB (FG set + BG set + italic: bits 0+1+3), X = 0x8 (bold)
+        // FFF = 0xF00 (red FG), BBB = 0x080 (green BG)
+        long attr = 0xB8F00080L;
+        Style style = ShellPanel.convertAttrToStyle(attr);
+        assertTrue(style.effectiveModifiers().contains(Modifier.BOLD));
+        assertTrue(style.effectiveModifiers().contains(Modifier.ITALIC));
+        assertTrue(style.fg().isPresent());
+        assertTrue(style.bg().isPresent());
+    }
+
+    private static String rawContent(Line line) {
+        StringBuilder sb = new StringBuilder();
+        for (Span span : line.spans()) {
+            sb.append(span.content());
+        }
+        return sb.toString();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
new file mode 100644
index 000000000000..ad690a17c6e0
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParserTest.java
@@ -0,0 +1,482 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StatusParserTest {
+
+    // ---- parseIntegration tests ----
+
+    @Test
+    void parseIntegrationMinimal() {
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "myIntegration");
+        context.put("version", "4.21.0");
+        context.put("phase", 5);
+        root.put("context", context);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertEquals("myIntegration", info.name);
+        assertEquals("4.21.0", info.camelVersion);
+        assertEquals(5, info.state);
+    }
+
+    @Test
+    void parseIntegrationReturnsNullWhenNoContext() {
+        JsonObject root = new JsonObject();
+        ProcessHandle ph = ProcessHandle.current();
+
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+        assertNull(info);
+    }
+
+    @Test
+    void parseIntegrationWithRoutes() {
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "routeTest");
+        root.put("context", context);
+
+        JsonArray routes = new JsonArray();
+        JsonObject route = new JsonObject();
+        route.put("routeId", "route1");
+        route.put("from", "timer:tick");
+        route.put("state", "Started");
+        routes.add(route);
+        root.put("routes", routes);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertEquals(1, info.routes.size());
+        assertEquals("route1", info.routes.get(0).routeId);
+        assertEquals("timer:tick", info.routes.get(0).from);
+        assertEquals("Started", info.routes.get(0).state);
+        assertEquals(1, info.routeTotal);
+        assertEquals(1, info.routeStarted);
+    }
+
+    @Test
+    void parseIntegrationWithHealth() {
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "healthTest");
+        root.put("context", context);
+
+        JsonObject healthChecks = new JsonObject();
+        healthChecks.put("ready", true);
+        JsonArray checks = new JsonArray();
+        JsonObject check = new JsonObject();
+        check.put("group", "readiness");
+        check.put("id", "context");
+        check.put("state", "UP");
+        check.put("readiness", true);
+        checks.add(check);
+        healthChecks.put("checks", checks);
+        root.put("healthChecks", healthChecks);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertEquals("1/1", info.ready);
+        assertEquals(1, info.healthChecks.size());
+        assertEquals("context", info.healthChecks.get(0).name);
+        assertEquals("UP", info.healthChecks.get(0).state);
+    }
+
+    @Test
+    void parseIntegrationWithEndpoints() {
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "endpointTest");
+        root.put("context", context);
+
+        JsonObject endpointsObj = new JsonObject();
+        JsonArray endpointList = new JsonArray();
+        JsonObject ep = new JsonObject();
+        ep.put("uri", "timer:tick");
+        ep.put("direction", "in");
+        ep.put("routeId", "route1");
+        ep.put("hits", 42L);
+        endpointList.add(ep);
+        endpointsObj.put("endpoints", endpointList);
+        root.put("endpoints", endpointsObj);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertEquals(1, info.endpoints.size());
+        assertEquals("timer:tick", info.endpoints.get(0).uri);
+        assertEquals("timer", info.endpoints.get(0).component);
+        assertEquals(42L, info.endpoints.get(0).hits);
+    }
+
+    @Test
+    void parseIntegrationWithCircuitBreakers() {
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "cbTest");
+        root.put("context", context);
+
+        JsonObject r4j = new JsonObject();
+        JsonArray breakers = new JsonArray();
+        JsonObject cb = new JsonObject();
+        cb.put("routeId", "route1");
+        cb.put("id", "cb1");
+        cb.put("state", "CLOSED");
+        cb.put("failureRate", 0.5);
+        breakers.add(cb);
+        r4j.put("circuitBreakers", breakers);
+        root.put("resilience4j", r4j);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertEquals(1, info.circuitBreakers.size());
+        assertEquals("resilience4j", info.circuitBreakers.get(0).component);
+        assertEquals("CLOSED", info.circuitBreakers.get(0).state);
+        assertEquals(0.5, info.circuitBreakers.get(0).failureRate);
+    }
+
+    @Test
+    void parseIntegrationWithErrors() {
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "errorTest");
+        root.put("context", context);
+
+        JsonObject errorsObj = new JsonObject();
+        errorsObj.put("size", 3);
+        root.put("errors", errorsObj);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertEquals(3, info.errorCount);
+    }
+
+    @Test
+    void parseIntegrationWithInflight() {
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "inflightTest");
+        root.put("context", context);
+
+        JsonObject inflightObj = new JsonObject();
+        inflightObj.put("inflightBrowseEnabled", true);
+        inflightObj.put("inflight", 1);
+        JsonArray exchanges = new JsonArray();
+        JsonObject ex = new JsonObject();
+        ex.put("exchangeId", "ID-1");
+        ex.put("fromRouteId", "route1");
+        ex.put("atRouteId", "route1");
+        ex.put("nodeId", "node1");
+        ex.put("elapsed", 100L);
+        ex.put("duration", 200L);
+        exchanges.add(ex);
+        inflightObj.put("exchanges", exchanges);
+        root.put("inflight", inflightObj);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertTrue(info.inflightBrowseEnabled);
+        assertEquals(1, info.inflightExchanges.size());
+        assertEquals("ID-1", info.inflightExchanges.get(0).exchangeId);
+        assertFalse(info.inflightExchanges.get(0).blocked);
+    }
+
+    @Test
+    void parseIntegrationWithHttpEndpoints() {
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "httpTest");
+        root.put("context", context);
+
+        JsonObject restsObj = new JsonObject();
+        JsonArray restList = new JsonArray();
+        JsonObject rest = new JsonObject();
+        rest.put("url", "http://localhost:8080/api/hello";);
+        rest.put("method", "get");
+        rest.put("routeId", "route1");
+        rest.put("hits", 10L);
+        restList.add(rest);
+        restsObj.put("rests", restList);
+        root.put("rests", restsObj);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertEquals(1, info.httpEndpoints.size());
+        assertEquals("GET", info.httpEndpoints.get(0).method);
+        assertTrue(info.httpEndpoints.get(0).fromRest);
+    }
+
+    @Test
+    void parseIntegrationMissingSectionsDoNotNpe() {
+        // A minimal JSON with only context — all other sections missing 
should not NPE
+        JsonObject root = new JsonObject();
+        JsonObject context = new JsonObject();
+        context.put("name", "minimal");
+        root.put("context", context);
+
+        ProcessHandle ph = ProcessHandle.current();
+        IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+
+        assertNotNull(info);
+        assertEquals("minimal", info.name);
+        assertTrue(info.routes.isEmpty());
+        assertTrue(info.healthChecks.isEmpty());
+        assertTrue(info.endpoints.isEmpty());
+        assertTrue(info.circuitBreakers.isEmpty());
+        assertTrue(info.inflightExchanges.isEmpty());
+    }
+
+    // ---- parseTraceEntry tests ----
+
+    @Test
+    void parseTraceEntryFirstDirection() {
+        JsonObject json = new JsonObject();
+        json.put("uid", "uid1");
+        json.put("exchangeId", "EX-1");
+        json.put("routeId", "route1");
+        json.put("nodeId", "node1");
+        json.put("first", true);
+        json.put("last", false);
+        json.put("done", false);
+        json.put("failed", false);
+        json.put("endpointUri", "timer:tick");
+        json.put("nodeLevel", 1);
+        json.put("elapsed", 42L);
+        json.put("timestamp", 1700000000000L);
+
+        TraceEntry entry = StatusParser.parseTraceEntry(json, "12345");
+        assertEquals("12345", entry.pid);
+        assertEquals("*-> ", entry.direction);
+        assertEquals("Processing", entry.status);
+        assertTrue(entry.processor.contains("from[timer:tick]"));
+        assertEquals(42L, entry.elapsed);
+    }
+
+    @Test
+    void parseTraceEntryLastDirection() {
+        JsonObject json = new JsonObject();
+        json.put("uid", "uid2");
+        json.put("exchangeId", "EX-2");
+        json.put("last", true);
+        json.put("first", false);
+        json.put("done", true);
+        json.put("failed", false);
+        json.put("nodeLabel", "log:result");
+        json.put("nodeLevel", 1);
+        json.put("timestamp", 1700000000000L);
+
+        TraceEntry entry = StatusParser.parseTraceEntry(json, "12345");
+        assertEquals("<-* ", entry.direction);
+        assertEquals("Done", entry.status);
+    }
+
+    @Test
+    void parseTraceEntryRemoteDirection() {
+        JsonObject json = new JsonObject();
+        json.put("uid", "uid3");
+        json.put("first", true);
+        json.put("last", false);
+        json.put("done", false);
+        json.put("failed", false);
+        json.put("remoteEndpoint", true);
+        json.put("endpointUri", "http://example.com";);
+        json.put("nodeLevel", 1);
+        json.put("timestamp", 1700000000000L);
+
+        TraceEntry entry = StatusParser.parseTraceEntry(json, "12345");
+        assertEquals("*-->", entry.direction);
+    }
+
+    @Test
+    void parseTraceEntryFailedStatus() {
+        JsonObject json = new JsonObject();
+        json.put("uid", "uid4");
+        json.put("first", false);
+        json.put("last", false);
+        json.put("done", true);
+        json.put("failed", true);
+        json.put("nodeLabel", "process");
+        json.put("nodeLevel", 0);
+
+        TraceEntry entry = StatusParser.parseTraceEntry(json, "12345");
+        assertEquals("Failed", entry.status);
+        assertTrue(entry.failed);
+    }
+
+    @Test
+    void parseTraceEntryWithException() {
+        JsonObject json = new JsonObject();
+        json.put("uid", "uid5");
+        json.put("first", false);
+        json.put("last", false);
+        json.put("done", true);
+        json.put("failed", true);
+        json.put("nodeLabel", "process");
+        json.put("nodeLevel", 0);
+
+        JsonObject exc = new JsonObject();
+        exc.put("message", "NullPointerException");
+        exc.put("stackTrace", "at Foo.bar(Foo.java:42)");
+        json.put("exception", exc);
+
+        TraceEntry entry = StatusParser.parseTraceEntry(json, "12345");
+        assertNotNull(entry.exception);
+        assertTrue(entry.exception.contains("NullPointerException"));
+        assertTrue(entry.exception.contains("at Foo.bar"));
+    }
+
+    @Test
+    void parseTraceEntryStubDirection() {
+        JsonObject json = new JsonObject();
+        json.put("uid", "uid6");
+        json.put("first", true);
+        json.put("last", false);
+        json.put("done", false);
+        json.put("failed", false);
+        json.put("stubEndpoint", true);
+        json.put("endpointUri", "stub:test");
+        json.put("nodeLevel", 1);
+
+        TraceEntry entry = StatusParser.parseTraceEntry(json, "12345");
+        assertEquals("~-->", entry.direction);
+    }
+
+    // ---- parseMessage tests ----
+
+    @Test
+    void parseMessageWithHeadersAsListOfKv() {
+        JsonObject message = new JsonObject();
+        JsonArray headers = new JsonArray();
+        JsonObject h1 = new JsonObject();
+        h1.put("key", "Content-Type");
+        h1.put("value", "text/plain");
+        h1.put("type", "java.lang.String");
+        headers.add(h1);
+        message.put("headers", headers);
+
+        StatusParser.MessageData md = StatusParser.parseMessage(message);
+        assertNotNull(md.headers());
+        assertEquals("text/plain", md.headers().get("Content-Type"));
+        assertEquals("String", md.headerTypes().get("Content-Type"));
+    }
+
+    @Test
+    void parseMessageWithHeadersAsMap() {
+        JsonObject message = new JsonObject();
+        Map<String, Object> headers = new LinkedHashMap<>();
+        headers.put("Accept", "application/json");
+        message.put("headers", headers);
+
+        StatusParser.MessageData md = StatusParser.parseMessage(message);
+        assertNotNull(md.headers());
+        assertEquals("application/json", md.headers().get("Accept"));
+    }
+
+    @Test
+    void parseMessageWithBodyAsJsonObject() {
+        JsonObject message = new JsonObject();
+        JsonObject body = new JsonObject();
+        body.put("value", "Hello World");
+        body.put("type", "java.lang.String");
+        message.put("body", body);
+
+        StatusParser.MessageData md = StatusParser.parseMessage(message);
+        assertEquals("Hello World", md.body());
+        assertEquals("String", md.bodyType());
+    }
+
+    @Test
+    void parseMessageWithBodyAsRawString() {
+        JsonObject message = new JsonObject();
+        message.put("body", "plain text body");
+
+        StatusParser.MessageData md = StatusParser.parseMessage(message);
+        assertEquals("plain text body", md.body());
+    }
+
+    @Test
+    void parseMessageWithExchangeProperties() {
+        JsonObject message = new JsonObject();
+        JsonArray props = new JsonArray();
+        JsonObject p1 = new JsonObject();
+        p1.put("key", "CamelToEndpoint");
+        p1.put("value", "direct:end");
+        p1.put("type", "java.lang.String");
+        props.add(p1);
+        message.put("exchangeProperties", props);
+
+        StatusParser.MessageData md = StatusParser.parseMessage(message);
+        assertNotNull(md.exchangeProperties());
+        assertEquals("direct:end", 
md.exchangeProperties().get("CamelToEndpoint"));
+    }
+
+    @Test
+    void parseMessageWithExchangeVariables() {
+        JsonObject message = new JsonObject();
+        JsonArray vars = new JsonArray();
+        JsonObject v1 = new JsonObject();
+        v1.put("key", "myVar");
+        v1.put("value", "varValue");
+        v1.put("type", "java.lang.String");
+        vars.add(v1);
+        message.put("exchangeVariables", vars);
+
+        StatusParser.MessageData md = StatusParser.parseMessage(message);
+        assertNotNull(md.exchangeVariables());
+        assertEquals("varValue", md.exchangeVariables().get("myVar"));
+    }
+
+    @Test
+    void parseMessageEmptyReturnsNulls() {
+        JsonObject message = new JsonObject();
+
+        StatusParser.MessageData md = StatusParser.parseMessage(message);
+        assertNull(md.headers());
+        assertNull(md.body());
+        assertNull(md.exchangeProperties());
+        assertNull(md.exchangeVariables());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelperTest.java
new file mode 100644
index 000000000000..f389fd6395dd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelperTest.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+
+import dev.tamboui.style.AnsiColor;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Modifier;
+import dev.tamboui.style.Style;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TuiHelperTest {
+
+    // ---- stripAnsi tests ----
+
+    @Test
+    void stripAnsiCsiColorCodes() {
+        // ESC[31m = red foreground, ESC[0m = reset
+        String input = "Hello";
+        assertEquals("Hello", TuiHelper.stripAnsi(input));
+    }
+
+    @Test
+    void stripAnsiTwoCharFeSequences() {
+        // ESC M = Reverse Index (a 2-char Fe sequence)
+        String input = "BeforeMAfter";
+        assertEquals("BeforeAfter", TuiHelper.stripAnsi(input));
+    }
+
+    @Test
+    void stripAnsiTabToEightSpaces() {
+        String input = "A\tB";
+        assertEquals("A        B", TuiHelper.stripAnsi(input));
+    }
+
+    @Test
+    void stripAnsiCarriageReturnRemoved() {
+        String input = "Hello\rWorld";
+        assertEquals("HelloWorld", TuiHelper.stripAnsi(input));
+    }
+
+    @Test
+    void stripAnsiPlainTextUnchanged() {
+        String input = "No escape codes here";
+        assertEquals("No escape codes here", TuiHelper.stripAnsi(input));
+    }
+
+    @Test
+    void stripAnsiNullAndEmpty() {
+        assertNull(TuiHelper.stripAnsi(null));
+        assertEquals("", TuiHelper.stripAnsi(""));
+    }
+
+    // ---- ansiToLine tests ----
+
+    @Test
+    void ansiToLineColorCodesProduceStyledSpans() {
+        // ESC[31m = ANSI Red foreground
+        String raw = "Error text";
+        Line line = TuiHelper.ansiToLine(raw, 0);
+        List<Span> spans = line.spans();
+        // Should have at least 2 spans: colored "Error" and unstyled " text"
+        assertTrue(spans.size() >= 2);
+        assertEquals("Error", spans.get(0).content());
+        assertEquals(Color.ansi(AnsiColor.RED), 
spans.get(0).style().fg().orElse(null));
+    }
+
+    @Test
+    void ansiToLineBoldItalicUnderline() {
+        // ESC[1m = bold, ESC[3m = italic, ESC[4m = underline
+        String raw = "Styled";
+        Line line = TuiHelper.ansiToLine(raw, 0);
+        List<Span> spans = line.spans();
+        assertTrue(spans.size() >= 1);
+        Style style = spans.get(0).style();
+        assertTrue(style.effectiveModifiers().contains(Modifier.BOLD));
+        assertTrue(style.effectiveModifiers().contains(Modifier.ITALIC));
+        assertTrue(style.effectiveModifiers().contains(Modifier.UNDERLINED));
+    }
+
+    @Test
+    void ansiToLine256Color() {
+        // ESC[38;5;196m = 256-color index 196 (red)
+        String raw = "Indexed";
+        Line line = TuiHelper.ansiToLine(raw, 0);
+        List<Span> spans = line.spans();
+        assertTrue(spans.size() >= 1);
+        assertEquals("Indexed", spans.get(0).content());
+        assertTrue(spans.get(0).style().fg().isPresent());
+    }
+
+    @Test
+    void ansiToLineRgbColor() {
+        // ESC[38;2;255;128;0m = RGB color
+        String raw = "RGB";
+        Line line = TuiHelper.ansiToLine(raw, 0);
+        List<Span> spans = line.spans();
+        assertTrue(spans.size() >= 1);
+        assertEquals("RGB", spans.get(0).content());
+        assertEquals(Color.rgb(255, 128, 0), 
spans.get(0).style().fg().orElse(null));
+    }
+
+    @Test
+    void ansiToLineResetClearsStyle() {
+        String raw = "Bold Normal";
+        Line line = TuiHelper.ansiToLine(raw, 0);
+        List<Span> spans = line.spans();
+        assertTrue(spans.size() >= 2);
+        
assertTrue(spans.get(0).style().effectiveModifiers().contains(Modifier.BOLD));
+        
assertFalse(spans.get(1).style().effectiveModifiers().contains(Modifier.BOLD));
+    }
+
+    @Test
+    void ansiToLineHSkipColumnsSkipped() {
+        // With hSkip=3, the first 3 visible columns should be omitted
+        String raw = "ABCDEF";
+        Line line = TuiHelper.ansiToLine(raw, 3);
+        String content = rawContent(line);
+        assertEquals("DEF", content);
+    }
+
+    @Test
+    void ansiToLineNullReturnsEmpty() {
+        Line line = TuiHelper.ansiToLine(null, 0);
+        assertEquals("", rawContent(line));
+    }
+
+    // ---- shortTypeName tests ----
+
+    @Test
+    void shortTypeNameJavaLangPrefix() {
+        assertEquals("String", TuiHelper.shortTypeName("java.lang.String"));
+        assertEquals("Integer", TuiHelper.shortTypeName("java.lang.Integer"));
+    }
+
+    @Test
+    void shortTypeNameJavaUtilPrefix() {
+        assertEquals("HashMap", TuiHelper.shortTypeName("java.util.HashMap"));
+        assertEquals("ArrayList", 
TuiHelper.shortTypeName("java.util.ArrayList"));
+    }
+
+    @Test
+    void shortTypeNameJavaUtilConcurrentPrefix() {
+        assertEquals("ConcurrentHashMap", 
TuiHelper.shortTypeName("java.util.concurrent.ConcurrentHashMap"));
+    }
+
+    @Test
+    void shortTypeNameCamelLongType() {
+        // This specific type is mapped to a short name
+        assertEquals("WrappedInputStream",
+                
TuiHelper.shortTypeName("org.apache.camel.converter.stream.CachedOutputStream.WrappedInputStream"));
+    }
+
+    @Test
+    void shortTypeNameCamelSupportPrefix() {
+        assertEquals("DefaultExchange", 
TuiHelper.shortTypeName("org.apache.camel.support.DefaultExchange"));
+    }
+
+    @Test
+    void shortTypeNameLongCustomType() {
+        // Types longer than 34 chars are shortened to the last segment
+        String longType = "com.example.very.long.package.name.MyClass";
+        assertEquals("MyClass", TuiHelper.shortTypeName(longType));
+    }
+
+    @Test
+    void shortTypeNameShortNamePassedThrough() {
+        assertEquals("int", TuiHelper.shortTypeName("int"));
+        assertEquals("byte[]", TuiHelper.shortTypeName("byte[]"));
+    }
+
+    @Test
+    void shortTypeNameNull() {
+        assertEquals("null", TuiHelper.shortTypeName(null));
+    }
+
+    @Test
+    void shortTypeNameGroupedExchangeList() {
+        assertEquals("GroupedExchangeList",
+                TuiHelper.shortTypeName(
+                        
"org.apache.camel.processor.aggregate.AbstractListAggregationStrategy.GroupedExchangeList"));
+    }
+
+    @Test
+    void shortTypeNameCachedOutputStream() {
+        assertEquals("CachedOutputStream",
+                
TuiHelper.shortTypeName("org.apache.camel.converter.stream.CachedOutputStream"));
+    }
+
+    // ---- objToLong tests ----
+
+    @Test
+    void objToLongFromNumber() {
+        assertEquals(42L, TuiHelper.objToLong(42));
+        assertEquals(100L, TuiHelper.objToLong(100L));
+    }
+
+    @Test
+    void objToLongFromString() {
+        assertEquals(99L, TuiHelper.objToLong("99"));
+    }
+
+    @Test
+    void objToLongFromNull() {
+        assertEquals(0L, TuiHelper.objToLong(null));
+    }
+
+    @Test
+    void objToLongFromInvalidString() {
+        assertEquals(0L, TuiHelper.objToLong("not-a-number"));
+    }
+
+    private static String rawContent(Line line) {
+        StringBuilder sb = new StringBuilder();
+        for (Span span : line.spans()) {
+            sb.append(span.content());
+        }
+        return sb.toString();
+    }
+}

Reply via email to