feat: add tests and compose
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
102
backend/uframe/tests/test_bigtitle.py
Normal file
102
backend/uframe/tests/test_bigtitle.py
Normal 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
|
||||
@@ -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"
|
||||
|
||||
191
backend/uframe/tests/test_navigation.py
Normal file
191
backend/uframe/tests/test_navigation.py
Normal 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
|
||||
154
backend/uframe/tests/test_themes.py
Normal file
154
backend/uframe/tests/test_themes.py
Normal 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
30
docker-compose.yml
Normal 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:
|
||||
Reference in New Issue
Block a user