From 82cb4a97866fc6fe227ef4d7f293bca1ed5a8411 Mon Sep 17 00:00:00 2001 From: dtoro Date: Fri, 3 Apr 2026 10:15:30 +0200 Subject: [PATCH] feat: add tests and compose --- backend/uframe/parser.py | 9 +- backend/uframe/tests/test_bigtitle.py | 102 +++++++++++++ backend/uframe/tests/test_components.py | 53 +++++++ backend/uframe/tests/test_navigation.py | 191 ++++++++++++++++++++++++ backend/uframe/tests/test_themes.py | 154 +++++++++++++++++++ docker-compose.yml | 30 ++++ 6 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 backend/uframe/tests/test_bigtitle.py create mode 100644 backend/uframe/tests/test_navigation.py create mode 100644 backend/uframe/tests/test_themes.py create mode 100644 docker-compose.yml diff --git a/backend/uframe/parser.py b/backend/uframe/parser.py index 837e24a..0920974 100644 --- a/backend/uframe/parser.py +++ b/backend/uframe/parser.py @@ -294,8 +294,13 @@ def _parse_line(keyword: str, args: list[str], line_num: int) -> IRNode: elif keyword == "sparkline": label = args[0] if args else "" vals_str = args[1] if len(args) > 1 else "" - values = [float(v) for v in vals_str.split(",") if v.strip()] if vals_str else [] - width = int(args[2]) if len(args) > 2 else 20 + values: list[float] = [] + if vals_str and "$" not in vals_str: + values = [float(v) for v in vals_str.split(",") if v.strip()] + try: + width = int(args[2]) if len(args) > 2 else 20 + except ValueError: + width = 20 return Sparkline(label=label, values=values, spark_width=width, source_line=line_num) diff --git a/backend/uframe/tests/test_bigtitle.py b/backend/uframe/tests/test_bigtitle.py new file mode 100644 index 0000000..da682da --- /dev/null +++ b/backend/uframe/tests/test_bigtitle.py @@ -0,0 +1,102 @@ +"""Tests for BigTitle and font rendering.""" + +import uframe +from uframe.fonts import render_big_text, get_text_width, FONTS, FONT_HEIGHTS + + +# --------------------------------------------------------------------------- +# Unit tests for font module +# --------------------------------------------------------------------------- + + +def test_font_registry(): + assert "block" in FONTS + assert "thin" in FONTS + assert "pixel" in FONTS + + +def test_font_heights(): + assert FONT_HEIGHTS["block"] == 6 + assert FONT_HEIGHTS["thin"] == 3 + assert FONT_HEIGHTS["pixel"] == 3 + + +def test_render_big_text_block(): + lines = render_big_text("HI", "block") + assert len(lines) == 6 + # Should contain block characters + assert any("█" in line for line in lines) + + +def test_render_big_text_thin(): + lines = render_big_text("AB", "thin") + assert len(lines) == 3 + assert any("┌" in line or "├" in line for line in lines) + + +def test_render_big_text_pixel(): + lines = render_big_text("OK", "pixel") + assert len(lines) == 3 + assert any("▀" in line or "█" in line for line in lines) + + +def test_get_text_width(): + w = get_text_width("A", "block") + assert w > 0 + w2 = get_text_width("AB", "block") + assert w2 > w # two chars wider than one + + +def test_kerning(): + w_tight = get_text_width("AB", "block", kerning=0) + w_normal = get_text_width("AB", "block", kerning=1) + w_wide = get_text_width("AB", "block", kerning=2) + assert w_tight < w_normal < w_wide + + +# --------------------------------------------------------------------------- +# Integration tests — bigtitle through compile pipeline +# --------------------------------------------------------------------------- + + +def test_bigtitle_block_font(): + source = '''page "Test" 80 + bigtitle "HI" block''' + result = uframe.compile(source) + assert "█" in result.ascii + lines = result.ascii.strip().split("\n") + assert len(lines) >= 6 # block font is 6 lines tall + + +def test_bigtitle_thin_font(): + source = '''page "Test" 80 + bigtitle "OK" thin''' + result = uframe.compile(source) + # Thin font uses box-drawing characters + assert any(c in result.ascii for c in "┌├┐┘└┬") + + +def test_bigtitle_pixel_font(): + source = '''page "Test" 80 + bigtitle "GO" pixel''' + result = uframe.compile(source) + assert any(c in result.ascii for c in "▀▄█") + + +def test_bigtitle_fallback_to_smaller_font(): + """When text is too wide for block font, should fall back to smaller fonts.""" + source = '''page "Test" 40 + bigtitle "ABCDEFGHIJ" block''' + result = uframe.compile(source) + # Should still render with a fallback font (thin uses box-drawing) + assert result.ascii.strip() != "" + # Thin font characters should be present (fell back from block) + assert any(c in result.ascii for c in "┌├┐┘└┬─") + + +def test_bigtitle_with_alignment(): + source = '''page "Test" 80 + bigtitle "HI" block + align center''' + result = uframe.compile(source) + assert "█" in result.ascii diff --git a/backend/uframe/tests/test_components.py b/backend/uframe/tests/test_components.py index 7584b44..8238502 100644 --- a/backend/uframe/tests/test_components.py +++ b/backend/uframe/tests/test_components.py @@ -62,6 +62,59 @@ def test_use_std_nav(): assert "About" in result.ascii +def test_use_std_status_bar(): + source = '''page "Test" 60 + use std/status-bar + status_bar "Uptime" 95 100''' + result = uframe.compile(source) + assert "Uptime" in result.ascii + assert "█" in result.ascii + + +def test_use_std_status_bar_item(): + source = '''page "Test" 60 + use std/status-bar + status_item "Gateway" online''' + result = uframe.compile(source) + assert "Gateway" in result.ascii + assert "●" in result.ascii + + +def test_use_std_status_bar_row(): + source = '''page "Test" 60 + use std/status-bar + status_row "East" online "West" offline''' + result = uframe.compile(source) + assert "East" in result.ascii + assert "West" in result.ascii + + +def test_use_std_network_peer_list(): + source = '''page "Test" 60 + use std/network + peer_list "Active Peers"''' + result = uframe.compile(source) + assert "Active Peers" in result.ascii + + +def test_use_std_form_search(): + source = '''page "Test" 60 + use std/form + search_form "search" "/page/results.mu"''' + result = uframe.compile(source) + assert "Search" in result.ascii + assert "results.mu" in result.micron + + +def test_use_std_form_login(): + source = '''page "Test" 60 + use std/form + login_form "/page/auth.mu"''' + result = uframe.compile(source) + assert "Login" in result.ascii + assert "auth.mu" in result.micron + + def test_unknown_component_ignored(): source = '''page "Test" 50 heading 1 "Hello" diff --git a/backend/uframe/tests/test_navigation.py b/backend/uframe/tests/test_navigation.py new file mode 100644 index 0000000..50f7db8 --- /dev/null +++ b/backend/uframe/tests/test_navigation.py @@ -0,0 +1,191 @@ +"""Tests for HNav and VNav navigation components.""" + +import uframe + + +# --------------------------------------------------------------------------- +# HNav — Horizontal Navigation +# --------------------------------------------------------------------------- + + +def test_hnav_bar(): + source = '''page "Test" 60 + hnav bar + item "Home" "/page/index.mu" active + item "About" "/page/about.mu" + item "Settings" "/page/settings.mu"''' + result = uframe.compile(source) + assert "Home" in result.ascii + assert "About" in result.ascii + assert "Settings" in result.ascii + # Bar style has borders + assert "┌" in result.ascii or "─" in result.ascii + + +def test_hnav_tabs(): + source = '''page "Test" 60 + hnav tabs + item "Dashboard" "/page/dash.mu" active + item "Logs" "/page/logs.mu"''' + result = uframe.compile(source) + assert "Dashboard" in result.ascii + assert "Logs" in result.ascii + + +def test_hnav_pills(): + source = '''page "Test" 60 + hnav pills + item "All" "/page/all.mu" active + item "Active" "/page/active.mu" + item "Stale" "/page/stale.mu"''' + result = uframe.compile(source) + assert "All" in result.ascii + assert "Active" in result.ascii + assert "Stale" in result.ascii + + +def test_hnav_breadcrumb(): + source = '''page "Test" 60 + hnav breadcrumb + item "Home" "/page/index.mu" + item "Network" "/page/network.mu" + item "Node Alpha" "/page/alpha.mu" active''' + result = uframe.compile(source) + assert "Home" in result.ascii + assert "Network" in result.ascii + assert "Node Alpha" in result.ascii + # Breadcrumb uses ▸ separator + assert "▸" in result.ascii + + +def test_hnav_underline(): + source = '''page "Test" 60 + hnav underline + item "Overview" "/page/overview.mu" active + item "Details" "/page/details.mu"''' + result = uframe.compile(source) + assert "Overview" in result.ascii + assert "Details" in result.ascii + # Active item has underline + assert "━" in result.ascii + + +def test_hnav_with_separator(): + source = '''page "Test" 60 + hnav bar + item "Home" "/page/index.mu" active + separator + item "Help" "/page/help.mu"''' + result = uframe.compile(source) + assert "Home" in result.ascii + assert "Help" in result.ascii + + +def test_hnav_micron_links(): + source = '''page "Test" 60 + hnav bar + item "Home" "/page/index.mu" + item "About" "/page/about.mu"''' + result = uframe.compile(source) + assert "index.mu" in result.micron + assert "about.mu" in result.micron + + +# --------------------------------------------------------------------------- +# VNav — Vertical Navigation +# --------------------------------------------------------------------------- + + +def test_vnav_list(): + source = '''page "Test" 60 + vnav list + item "Dashboard" "/page/dash.mu" active + item "Peers" "/page/peers.mu" + item "Routes" "/page/routes.mu"''' + result = uframe.compile(source) + assert "Dashboard" in result.ascii + assert "Peers" in result.ascii + assert "Routes" in result.ascii + # Active item has marker + assert "▸" in result.ascii + + +def test_vnav_boxed(): + source = '''page "Test" 60 + vnav boxed + item "Home" "/page/index.mu" active + item "Settings" "/page/settings.mu"''' + result = uframe.compile(source) + assert "Home" in result.ascii + assert "Settings" in result.ascii + # Boxed style has borders + assert "┌" in result.ascii + assert "└" in result.ascii + + +def test_vnav_tree(): + source = '''page "Test" 60 + vnav tree + item "Root" "/page/root.mu" active + item "Child A" "/page/a.mu" + item "Child B" "/page/b.mu"''' + result = uframe.compile(source) + assert "Root" in result.ascii + assert "Child A" in result.ascii + assert "Child B" in result.ascii + # Tree style has connectors + assert "├" in result.ascii or "└" in result.ascii + + +def test_vnav_sidebar(): + source = '''page "Test" 60 + vnav sidebar + item "Overview" "/page/overview.mu" active + item "Metrics" "/page/metrics.mu" + item "Alerts" "/page/alerts.mu"''' + result = uframe.compile(source) + assert "Overview" in result.ascii + assert "Metrics" in result.ascii + assert "Alerts" in result.ascii + + +def test_vnav_minimal(): + source = '''page "Test" 60 + vnav minimal + item "Page 1" "/page/1.mu" + item "Page 2" "/page/2.mu" active + item "Page 3" "/page/3.mu"''' + result = uframe.compile(source) + assert "Page 1" in result.ascii + assert "Page 2" in result.ascii + assert "Page 3" in result.ascii + + +def test_vnav_with_separator(): + source = '''page "Test" 60 + vnav list + item "Home" "/page/index.mu" active + separator + item "Help" "/page/help.mu"''' + result = uframe.compile(source) + assert "Home" in result.ascii + assert "Help" in result.ascii + assert "─" in result.ascii + + +def test_vnav_with_width(): + source = '''page "Test" 60 + vnav boxed 25 + item "Nav Item" "/page/nav.mu" active''' + result = uframe.compile(source) + assert "Nav Item" in result.ascii + + +def test_vnav_micron_links(): + source = '''page "Test" 60 + vnav list + item "Home" "/page/index.mu" + item "About" "/page/about.mu"''' + result = uframe.compile(source) + assert "index.mu" in result.micron + assert "about.mu" in result.micron diff --git a/backend/uframe/tests/test_themes.py b/backend/uframe/tests/test_themes.py new file mode 100644 index 0000000..58e000c --- /dev/null +++ b/backend/uframe/tests/test_themes.py @@ -0,0 +1,154 @@ +"""Tests for the theme system.""" + +import uframe +from uframe.themes import ( + get_theme, ThemeDef, BUILTIN_THEMES, + THEME_DEFAULT, THEME_NOUVEAU, THEME_GOTHIC, + THEME_BAMBOO, THEME_CIRCUIT, THEME_BRUTALIST, +) + + +# --------------------------------------------------------------------------- +# Theme registry +# --------------------------------------------------------------------------- + + +def test_builtin_themes_exist(): + assert len(BUILTIN_THEMES) == 6 + for name in ("default", "nouveau", "gothic", "bamboo", "circuit", "brutalist"): + assert name in BUILTIN_THEMES + + +def test_get_theme_by_name(): + theme = get_theme("gothic") + assert theme.name == "gothic" + + +def test_get_theme_case_insensitive(): + theme = get_theme("GOTHIC") + assert theme.name == "gothic" + + +def test_get_theme_unknown_returns_default(): + theme = get_theme("nonexistent") + assert theme.name == "default" + + +# --------------------------------------------------------------------------- +# Theme structure +# --------------------------------------------------------------------------- + + +def test_theme_has_all_fields(): + for name, theme in BUILTIN_THEMES.items(): + assert theme.borders_light is not None + assert theme.borders_heavy is not None + assert theme.borders_double is not None + assert theme.borders_rounded is not None + assert theme.dividers is not None + assert theme.indicators is not None + assert theme.gauge is not None + assert theme.form is not None + assert theme.palette is not None + + +def test_border_chars_method(): + theme = THEME_DEFAULT + bc = theme.border_chars("light") + assert bc.tl == "┌" + assert bc.tr == "┐" + + +def test_border_dict_method(): + theme = THEME_DEFAULT + bd = theme.border_dict("double") + assert bd["tl"] == "╔" + assert bd["h"] == "═" + + +# --------------------------------------------------------------------------- +# Themed rendering — each theme produces correct characters +# --------------------------------------------------------------------------- + + +def test_compile_with_default_theme(): + source = '''page "Test" 40 + box light "Panel" + text "Content"''' + result = uframe.compile(source) + assert "┌" in result.ascii # default light border + + +def test_compile_with_gothic_theme(): + source = '''page "Test" 40 + box light "Panel" + text "Content"''' + result = uframe.compile(source, theme="gothic") + # Gothic uses double-style borders for light + assert "╔" in result.ascii + + +def test_compile_with_brutalist_theme(): + source = '''page "Test" 40 + box light "Panel" + text "Content"''' + result = uframe.compile(source, theme="brutalist") + # Brutalist uses block characters for light borders + assert "▛" in result.ascii + + +def test_compile_with_bamboo_theme(): + source = '''page "Test" 40 + box light "Panel" + text "Content"''' + result = uframe.compile(source, theme="bamboo") + # Bamboo uses dashed light borders + assert "╌" in result.ascii or "┌" in result.ascii + + +def test_compile_with_circuit_theme(): + source = '''page "Test" 40 + gauge "CPU" 50 100 20''' + result = uframe.compile(source, theme="circuit") + # Circuit uses ▰/▱ for gauge + assert "▰" in result.ascii + assert "▱" in result.ascii + + +def test_compile_with_nouveau_theme(): + source = '''page "Test" 40 + status "Server" online''' + result = uframe.compile(source, theme="nouveau") + # Nouveau uses ❀ for online indicator + assert "❀" in result.ascii + + +def test_theme_divider_chars(): + source = '''page "Test" 40 + divider heavy''' + result_default = uframe.compile(source) + result_gothic = uframe.compile(source, theme="gothic") + # Both should render but potentially with different chars + assert "━" in result_default.ascii + assert "═" in result_gothic.ascii + + +def test_theme_gauge_chars(): + source = '''page "Test" 40 + gauge "Test" 50 100 20''' + result_default = uframe.compile(source) + result_brutalist = uframe.compile(source, theme="brutalist") + # Default: █/░, Brutalist: █/(space) + assert "█" in result_default.ascii + assert "█" in result_brutalist.ascii + + +def test_theme_form_chars(): + source = '''page "Test" 50 + form "test" + checkbox "agree" "Accept"''' + result_default = uframe.compile(source) + result_gothic = uframe.compile(source, theme="gothic") + # Default: [ ], Gothic: ⚐ + assert "[ ]" in result_default.ascii or "Accept" in result_default.ascii + assert "⚐" in result_gothic.ascii or "Accept" in result_gothic.ascii diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..872cead --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +services: + micronomicon: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + environment: + - PAGES_DIR=/data/pages + - SOURCES_DIR=/data/sources + - NOMADNET_CONTAINER=nomadnet + volumes: + - pages:/data/pages + - sources:/data/sources + - /var/run/docker.sock:/var/run/docker.sock:ro + restart: unless-stopped + depends_on: + - nomadnet + + nomadnet: + image: ghcr.io/markqvist/nomadnet:latest + volumes: + - pages:/root/.nomadnetwork/storage/pages + - nomadnet-config:/root/.nomadnetwork + restart: unless-stopped + +volumes: + pages: + sources: + nomadnet-config: