feat: add tests and compose

This commit is contained in:
2026-04-03 10:15:30 +02:00
parent f0b0a5dd24
commit 82cb4a9786
6 changed files with 537 additions and 2 deletions

View File

@@ -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 []
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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

30
docker-compose.yml Normal file
View File

@@ -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: