Skip to content

Commit ec70e3e

Browse files
test: add comprehensive _build_fire_art tests for heat, style, and geometry
- Verify exact heat row wave character content (kills ≈→XX≈XX, ~→XX~XX) - Check heat row bright_red style (kills style mutations) - Verify heat rows reduce flame budget while maintaining total height - Assert dim style on all structural frame elements (top edge, borders, hearth) - Test flame row centering, min width, and trailing pad calculations - Test exact boundary: flame_rows_effective == num_defs (kills >= vs >) - Verify default parameter values produce expected styling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 711c25d commit ec70e3e

1 file changed

Lines changed: 262 additions & 0 deletions

File tree

tests/test_fireplace_visual.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from flameconnect.models import FlameColor, RGBWColor
66
from flameconnect.tui.widgets import (
77
_FIXED_ROWS,
8+
_FLAME_DEFS,
89
_FLAME_PALETTES,
10+
_HEAT_ROWS,
911
_MIN_FLAME_ROWS,
1012
_build_fire_art,
1113
_expand_flame,
@@ -425,6 +427,266 @@ def test_heat_off_no_wave_chars(self):
425427
assert "\u2248" not in plain
426428
assert "~" not in plain
427429

430+
def test_heat_rows_exact_wave_content(self):
431+
"""Heat rows contain exact wave characters (kills ≈ → XX≈XX)."""
432+
w = 50
433+
text = _build_fire_art(w, 20, heat_on=True)
434+
plain = text.plain
435+
lines = plain.split("\n")
436+
ow = w - 2 # outer width
437+
# Heat rows are at the top, before the ▁ top edge
438+
heat_lines = []
439+
for line in lines:
440+
if "\u2248" in line or "~" in line:
441+
heat_lines.append(line)
442+
assert len(heat_lines) == _HEAT_ROWS
443+
# Each heat row: " " + wave_chars * ow + " "
444+
for line in heat_lines:
445+
assert line[0] == " "
446+
assert line[-1] == " "
447+
inner = line[1:-1]
448+
assert len(inner) == ow
449+
# Must be pure ≈ or pure ~ (kills XX≈XX and XX~XX mutations)
450+
assert inner == "\u2248" * ow or inner == "~" * ow
451+
452+
def test_heat_row_style_is_bright_red(self):
453+
"""Heat wave chars should have bright_red style."""
454+
w = 50
455+
text = _build_fire_art(w, 20, heat_on=True)
456+
plain = text.plain
457+
# Find the first ≈ or ~
458+
for idx, ch in enumerate(plain):
459+
if ch in ("\u2248", "~"):
460+
style = _style_at(text, idx)
461+
assert "bright_red" in style
462+
return
463+
raise AssertionError("No heat wave character found")
464+
465+
def test_heat_rows_reduce_flame_budget(self):
466+
"""Heat rows reduce flame rows, keeping total height constant."""
467+
w, h = 50, 20
468+
text_no_heat = _build_fire_art(w, h, heat_on=False)
469+
text_heat = _build_fire_art(w, h, heat_on=True)
470+
assert len(text_no_heat.plain.split("\n")) == h
471+
assert len(text_heat.plain.split("\n")) == h
472+
473+
474+
# ---------------------------------------------------------------------------
475+
# _build_fire_art – structural style verification
476+
# ---------------------------------------------------------------------------
477+
478+
479+
class TestBuildFireArtStyles:
480+
"""Verify dim style on structural frame elements."""
481+
482+
def test_top_edge_has_dim_style(self):
483+
"""Top edge ▁ characters should have 'dim' style."""
484+
text = _build_fire_art(50, 20)
485+
plain = text.plain
486+
idx = plain.index("\u2581")
487+
assert "dim" in _style_at(text, idx)
488+
489+
def test_outer_frame_top_has_dim_style(self):
490+
"""Outer frame ┌ should have 'dim' style."""
491+
text = _build_fire_art(50, 20)
492+
plain = text.plain
493+
idx = plain.index("\u250c")
494+
assert "dim" in _style_at(text, idx)
495+
496+
def test_outer_frame_bottom_has_dim_style(self):
497+
"""Outer frame └ should have 'dim' style."""
498+
text = _build_fire_art(50, 20)
499+
plain = text.plain
500+
idx = plain.index("\u2514")
501+
assert "dim" in _style_at(text, idx)
502+
503+
def test_inner_borders_have_dim_style(self):
504+
"""│ border characters should have 'dim' style."""
505+
text = _build_fire_art(50, 20)
506+
plain = text.plain
507+
idx = plain.index("\u2502")
508+
assert "dim" in _style_at(text, idx)
509+
510+
def test_outer_hearth_has_dim_style(self):
511+
"""Outer hearth ▓ row should have 'dim' style."""
512+
text = _build_fire_art(50, 20)
513+
plain = text.plain
514+
lines = plain.split("\n")
515+
for line in lines:
516+
if (
517+
line.startswith("\u2502")
518+
and not line.startswith("\u2502\u2502")
519+
and "\u2593" in line
520+
):
521+
# Outer hearth line
522+
line_offset = plain.index(line)
523+
hearth_idx = line.index("\u2593")
524+
assert "dim" in _style_at(text, line_offset + hearth_idx)
525+
return
526+
raise AssertionError("No outer hearth row found")
527+
528+
529+
# ---------------------------------------------------------------------------
530+
# _build_fire_art – flame centering and geometry
531+
# ---------------------------------------------------------------------------
532+
533+
534+
class TestBuildFireArtFlameGeometry:
535+
"""Verify flame row centering and width calculations."""
536+
537+
def test_flame_rows_centered(self):
538+
"""With fire_on, flame content is approximately centered."""
539+
w = 60
540+
text = _build_fire_art(w, 20, fire_on=True)
541+
plain = text.plain
542+
lines = plain.split("\n")
543+
for line in lines:
544+
if not line.startswith("\u2502\u2502"):
545+
continue
546+
if not line.endswith("\u2502\u2502"):
547+
continue
548+
inner = line[2:-2]
549+
# Skip LED, media, blank
550+
if "\u2591" in inner or "\u2593" in inner or inner.strip() == "":
551+
continue
552+
# Flame row: leading spaces should be similar to trailing
553+
leading = len(inner) - len(inner.lstrip())
554+
trailing = len(inner) - len(inner.rstrip())
555+
# Lead should be within reasonable range (not off by more than half)
556+
total_pad = leading + trailing
557+
if total_pad > 0:
558+
assert leading <= total_pad, (
559+
f"Centering broken: lead={leading}, trail={trailing}"
560+
)
561+
562+
def test_flame_rows_have_content_when_fire_on(self):
563+
"""Fire-on produces non-blank flame rows with flame chars."""
564+
w = 60
565+
text = _build_fire_art(w, 20, fire_on=True)
566+
plain = text.plain
567+
lines = plain.split("\n")
568+
flame_rows = []
569+
for line in lines:
570+
if not line.startswith("\u2502\u2502"):
571+
continue
572+
if not line.endswith("\u2502\u2502"):
573+
continue
574+
inner = line[2:-2]
575+
if "\u2591" in inner or "\u2593" in inner:
576+
continue
577+
if inner.strip():
578+
flame_rows.append(inner)
579+
assert len(flame_rows) >= _MIN_FLAME_ROWS
580+
581+
def test_exact_boundary_flame_rows_effective_equals_num_defs(self):
582+
"""When flame_rows_effective == num_defs, all defs render (kills >= vs >)."""
583+
num_defs = len(_FLAME_DEFS)
584+
h = num_defs + _FIXED_ROWS
585+
text = _build_fire_art(50, h, fire_on=True)
586+
lines = text.plain.split("\n")
587+
assert len(lines) == h
588+
# Count flame rows (non-blank inner content)
589+
flame_count = 0
590+
for line in lines:
591+
if line.startswith("\u2502\u2502") and line.endswith("\u2502\u2502"):
592+
inner = line[2:-2]
593+
if "\u2591" not in inner and "\u2593" not in inner and inner.strip():
594+
flame_count += 1
595+
# Should have exactly num_defs flame rows, no blanks above
596+
assert flame_count == num_defs
597+
598+
def test_flame_row_min_width_respected(self):
599+
"""Flame rows should be at least as wide as their min atom content."""
600+
w = 60
601+
text = _build_fire_art(w, 20, fire_on=True)
602+
plain = text.plain
603+
lines = plain.split("\n")
604+
for line in lines:
605+
if not line.startswith("\u2502\u2502"):
606+
continue
607+
if not line.endswith("\u2502\u2502"):
608+
continue
609+
inner = line[2:-2]
610+
if "\u2591" in inner or "\u2593" in inner or inner.strip() == "":
611+
continue
612+
# Content should not be shorter than atom chars
613+
content = inner.strip()
614+
assert len(content) > 0
615+
616+
def test_flame_row_trailing_pad_not_negative(self):
617+
"""Trailing pad should never be negative (uses max(..., 0))."""
618+
# Use a narrow width to stress the trailing pad calculation
619+
w = 40
620+
text = _build_fire_art(w, 20, fire_on=True)
621+
plain = text.plain
622+
iw = w - 4
623+
lines = plain.split("\n")
624+
for line in lines:
625+
if not line.startswith("\u2502\u2502"):
626+
continue
627+
if not line.endswith("\u2502\u2502"):
628+
continue
629+
inner = line[2:-2]
630+
if "\u2591" in inner or "\u2593" in inner:
631+
continue
632+
# Inner should be at least iw wide (may be wider for flame min_w)
633+
assert len(inner) >= iw
634+
635+
636+
# ---------------------------------------------------------------------------
637+
# _build_fire_art – default parameter values
638+
# ---------------------------------------------------------------------------
639+
640+
641+
class TestBuildFireArtDefaults:
642+
"""Verify default parameter values produce expected output."""
643+
644+
def test_default_fire_on_shows_flames(self):
645+
"""Default fire_on=True produces flame chars."""
646+
text = _build_fire_art(50, 20)
647+
flame_chars = set("()\\/|")
648+
assert any(ch in text.plain for ch in flame_chars)
649+
650+
def test_default_heat_off_no_waves(self):
651+
"""Default heat_on=False produces no wave chars."""
652+
text = _build_fire_art(50, 20)
653+
assert "\u2248" not in text.plain
654+
assert "~" not in text.plain
655+
656+
def test_default_led_style_is_dim(self):
657+
"""Default led_style='dim' applied to LED strip."""
658+
text = _build_fire_art(50, 20)
659+
plain = text.plain
660+
idx = plain.index("\u2591")
661+
assert "dim" in _style_at(text, idx)
662+
663+
def test_default_media_style_is_red(self):
664+
"""Default media_style='red' applied to media bed."""
665+
text = _build_fire_art(50, 20)
666+
plain = text.plain
667+
lines = plain.split("\n")
668+
for line in lines:
669+
if (
670+
line.startswith("\u2502\u2502")
671+
and line.endswith("\u2502\u2502")
672+
and "\u2593" in line
673+
):
674+
inner_start = 2
675+
line_offset = plain.index(line)
676+
for rel, ch in enumerate(line[inner_start:]):
677+
if ch == "\u2593":
678+
abs_offset = line_offset + inner_start + rel
679+
assert "red" in _style_at(text, abs_offset)
680+
return
681+
raise AssertionError("No inner media bed found")
682+
683+
def test_default_anim_frame_0(self):
684+
"""Default anim_frame=0 uses unrotated palette."""
685+
# Frame 0 uses original palette order
686+
text = _build_fire_art(50, 20, fire_on=True)
687+
# Just verify it produces valid output
688+
assert len(text.plain.split("\n")) == 20
689+
428690

429691
def _style_at(text, offset: int) -> str:
430692
"""Return the style string applied to the character at *offset*."""

0 commit comments

Comments
 (0)