hollow / tests /test_render.py
Pabloler21's picture
feat(game): 'what it remembers' weaves the memories it stole from you
03129db
Raw
History Blame Contribute Delete
15 kB
import html as _html_mod
from render import render_entity, render_treasure, render_recovered
def test_entity_renders_silhouette_scene():
from render import render_entity
html = render_entity(20)
assert 'entity-silhouette' in html # the new free-standing figure class
assert 'entity-portal' not in html # the gothic arch is gone
def test_entity_cue_marker():
from render import render_entity
assert 'data-cue="recall"' in render_entity(60, cue="recall", seq=3)
assert 'data-seq="3"' in render_entity(60, cue="recall", seq=3)
# no cue → no marker
assert 'cue-now' not in render_entity(20)
class TestRenderRecovered:
def test_empty_shows_nothing(self):
out = render_recovered(0)
assert "remembers nothing of itself" in out.lower()
assert "drawer-head" in out
def test_reveals_fragments_in_order(self):
import html as _html
from character import OWN_FRAGMENTS
out = render_recovered(2)
assert _html.escape(OWN_FRAGMENTS[0], quote=True) in out
assert _html.escape(OWN_FRAGMENTS[1], quote=True) in out
assert _html.escape(OWN_FRAGMENTS[2], quote=True) not in out
def test_capped_at_total_fragments(self):
from character import OWN_FRAGMENTS
out = render_recovered(99)
assert out.count('class="recovered-item') == len(OWN_FRAGMENTS)
def test_recovered_weaves_stolen(self):
from character import OWN_FRAGMENTS
html = render_recovered(1, claimed=["a red bicycle, the summer i turned seven"])
assert _html_mod.escape(OWN_FRAGMENTS[0], quote=True) in html # its real past
assert "a red bicycle" in html # the stolen memory
assert "stolen" in html # marked
# back-compat: no claimed arg still works
assert "remembers nothing of itself" in render_recovered(0).lower()
class TestRenderTreasure:
def test_empty_shows_placeholder(self):
out = render_treasure([])
assert "nothing yet. tell me something true." in out
def test_empty_has_no_memory_item(self):
# The memory-item style marker must not appear when there are no memories
out = render_treasure([])
assert "border-left:2px solid #5a3a7a" not in out
def test_single_memory_appears(self):
out = render_treasure(["has a dog named Nala"])
assert "has a dog named Nala" in out
def test_multiple_memories_all_appear(self):
out = render_treasure(["has a dog named Nala", "grew up near the sea"])
assert "has a dog named Nala" in out
assert "grew up near the sea" in out
def test_html_tags_are_escaped(self):
out = render_treasure(["<script>alert('x')</script>"])
assert "<script>" not in out
assert "&lt;script&gt;" in out
def test_ampersand_is_escaped(self):
out = render_treasure(["mom & dad"])
assert "mom &amp; dad" in out
def test_header_present_in_both_states(self):
assert "Treasure" in render_treasure([])
assert "Treasure" in render_treasure(["x"])
class TestRenderEntity:
def test_idle_contains_scene_and_webp_image(self):
out = render_entity(20)
assert "entity-scene" in out
assert "entity-silhouette" in out
assert "data:image/webp;base64," in out
def test_low_affinity_is_dark_and_faint(self):
out = render_entity(0)
assert "brightness(0.72)" in out
assert "opacity:0.82" in out
assert "scale(" not in out # size never changes — frame always fits
def test_full_affinity_is_bright_full(self):
out = render_entity(100)
assert "brightness(1.0)" in out
assert "opacity:1.0" in out
assert "scale(" not in out
def test_affinity_is_clamped(self):
assert render_entity(-10) == render_entity(0)
assert render_entity(140) == render_entity(100)
def test_sway_present_below_almost_tier(self):
assert "entity-sway" in render_entity(75)
def test_sway_removed_at_almost_tier(self):
assert "entity-sway" not in render_entity(76)
def test_almost_face_at_almost_tier(self):
# the silhouette itself swaps to the "almost" face at high bond
assert "entity-face-almost" in render_entity(90)
def test_flash_overlay_only_in_flash_modes(self):
assert "entity-flash" not in render_entity(40, "idle")
assert "entity-flash" in render_entity(40, "flash")
assert "entity-flash-strong" in render_entity(40, "flash_strong")
assert "entity-flash-strong" not in render_entity(40, "flash")
def test_flash_flickers_through_every_face(self):
# the flash stacks all four portraits to convulse between them
out = render_entity(0, "flash")
assert "flick-0" in out and "flick-3" in out
assert out.count("entity-flick") == 4
def test_end_mode_shows_end_image_only(self):
out = render_entity(100, "end")
assert "entity-end" in out
assert "entity-sway" not in out
assert "entity-flash" not in out
class TestRenderTreasureFinale:
def test_struck_memory_is_crossed_out(self):
out = render_treasure(["had a dog named Nala", "grew up near the sea"],
struck={"had a dog named Nala"})
assert "line-through" in out
def test_unstruck_render_has_no_strike(self):
out = render_treasure(["had a dog named Nala"])
assert "line-through" not in out
def test_mine_replaces_all_items(self):
out = render_treasure(["had a dog named Nala"], mine=True)
assert "...mine now." in out
assert "had a dog named Nala" not in out
class TestRenderEntityUX:
def test_fully_sharp_by_bond_45(self):
out = render_entity(45)
assert "brightness(1.0)" in out
assert "opacity:1.0" in out
def test_recognizable_silhouette_at_start(self):
# bond 20 (session start) must already read as a child, not a smudge
out = render_entity(20)
# t = 20/45 ≈ 0.444; brightness = 0.72 + 0.28*0.444 ≈ 0.84
assert "brightness(0.84)" in out
# opacity = 0.82 + 0.18*0.444 ≈ 0.90
assert "opacity:0.9" in out
def test_scene_tone_class_present(self):
assert 'class="entity-scene tone-warm"' in render_entity(50, tone=20)
assert 'class="entity-scene tone-hostile"' in render_entity(50, tone=-30)
def test_flash_animation_key_alternates_by_seq(self):
assert "flash-0" in render_entity(40, "flash", seq=0)
assert "flash-1" in render_entity(40, "flash", seq=1)
assert "flash-0" in render_entity(40, "flash", seq=2)
def test_ghost_echo_only_on_strong_flash_and_end(self):
assert "entity-ghost" not in render_entity(40, "idle")
assert "entity-ghost" not in render_entity(40, "flash")
assert "entity-ghost" in render_entity(40, "flash_strong")
assert "entity-ghost" in render_entity(95, "end")
class TestRenderTreasureUX:
def test_panel_has_scroll_class(self):
assert "treasure-panel" in render_treasure([])
assert "treasure-panel" in render_treasure(["had a dog named Nala"])
def test_older_memories_fade(self):
out = render_treasure(["oldest memory", "middle memory", "newest memory"])
assert "opacity:0.86" in out # oldest of three: 1 - 0.07*2
assert "opacity:1.0" in out # newest stays fully lit
def test_age_fade_has_floor(self):
out = render_treasure([f"memory {i}" for i in range(20)])
assert "opacity:0.45" in out
def test_claimed_memories_marked(self):
out = render_treasure(["mine still", "taken one"], claimed={"taken one"})
assert "border-left-color:#3a2a50" in out
def test_unclaimed_render_unmarked(self):
out = render_treasure(["mine still"])
assert "border-left-color:#3a2a50" not in out
def test_just_claimed_item_gets_claiming_class(self):
out = render_treasure(["mine still", "taken one"],
claimed={"taken one"},
just_claimed="taken one")
assert "claiming" in out
assert "class=\"treasure-item claiming" in out
class TestTreasureWounds:
def test_redacted_entry_per_wound(self):
html_out = render_treasure([], wounds=["you freak", "you bore me"])
assert html_out.count("▮") >= 8 # two entries of 4-7 blocks
def test_redacted_text_never_in_dom(self):
html_out = render_treasure([], wounds=["you freak"])
assert "you freak" not in html_out # no inspect-element spoiler
def test_revealed_wound_shows_text(self):
html_out = render_treasure([], wounds=["you freak", "you bore me"], revealed=1)
assert "you freak" in html_out
assert "you bore me" not in html_out
def test_revealed_wound_is_escaped(self):
html_out = render_treasure([], wounds=["<b>rude</b>"], revealed=1)
assert "<b>" not in html_out
assert "&lt;b&gt;" in html_out
def test_yours_mode_changes_header(self):
html_out = render_treasure(["a memory"], wounds=["w1"], revealed=1, yours=True)
assert "Your Words" in html_out
assert "a memory" not in html_out # memories discarded in yours mode
def test_wounds_render_alongside_memories(self):
html_out = render_treasure(["had a dog"], wounds=["w1"])
assert "had a dog" in html_out
assert "▮" in html_out
def test_no_wounds_is_backward_compatible(self):
assert render_treasure(["had a dog"]) == render_treasure(["had a dog"], wounds=[])
def test_wounds_alone_suppress_empty_placeholder(self):
html_out = render_treasure([], wounds=["w1"])
assert "nothing yet" not in html_out
class TestMaterialization:
def test_low_bond_uses_darkness_and_fog_not_blur(self):
html = render_entity(10, "idle", seq=0) # barely materialized
assert "blur(" not in html # no loading-look blur
assert "brightness(" in html # shadowed instead
def test_high_bond_is_bright_and_clear(self):
html = render_entity(45, "idle", seq=0)
assert "brightness(1" in html or "brightness(0.9" in html
class TestEntityPeaceModes:
def test_peace_shows_peace_face_no_ghost(self):
out = render_entity(90, "peace")
assert "entity-peace" in out
assert "entity-ghost" not in out # no menace in the redemption
def test_peace_dissolve_combines_peace_and_dissolve(self):
out = render_entity(90, "peace_dissolve")
assert "entity-peace" in out
assert "entity-dissolve" in out
def test_idle_has_no_dissolve(self):
assert "entity-dissolve" not in render_entity(50, "idle")
class TestEntityRageMode:
def test_rage_mode_shows_rage_face_with_tint(self):
out = render_entity(12, "rage")
assert "entity-rage" in out
assert "entity-rage-tint" in out
def test_rage_mode_has_ghost_echo(self):
assert "entity-ghost" in render_entity(12, "rage")
def test_idle_has_no_rage_artifacts(self):
out = render_entity(50, "idle")
assert "entity-rage" not in out
class TestEntityFrenzyMode:
def test_frenzy_stacks_all_faces(self):
out = render_entity(12, "frenzy")
assert "entity-frenzy-wrap" in out
assert out.count("entity-flick") == 4
def test_frenzy_hides_rage_face_beneath(self):
# the reveal happens AFTER the convulsion — the smudge stays under it
out = render_entity(12, "frenzy")
assert 'class="entity-img entity-rage"' not in out
assert "entity-rage-tint" in out
def test_frenzy_has_triple_screen_stab_and_sting(self):
out = render_entity(12, "frenzy")
assert "entity-ghost-frenzy" in out
assert "cue-audio" in out # the sting (queued)
class TestReplyChime:
def test_idle_has_no_audio(self):
# the per-reply chime was removed — the child SPEAKS every reply now,
# so the chime was redundant (and fired out of sync under streaming)
assert "<audio" not in render_entity(50, "idle", seq=3)
def test_idle_is_stable_across_seq(self):
# with no chime, idle renders identically regardless of the turn counter
assert render_entity(50, "idle", seq=0) == render_entity(50, "idle", seq=1)
def test_finale_modes_have_no_chime(self):
for mode in ("end", "rage", "peace", "peace_dissolve"):
out = render_entity(90, mode)
assert "<audio autoplay" not in out, mode
def test_frenzy_keeps_only_the_sting(self):
out = render_entity(12, "frenzy")
assert out.count("cue-audio") == 1
assert "<audio autoplay" not in out # the sting routes via the queue
class TestBuildMode:
def test_build_loops_the_heartbeat_and_not_the_chime(self):
html = render_entity(50, "build", seq=4)
assert "<audio autoplay loop" in html # heartbeat bed loops
assert "entity-redpulse" in html # red suspense pulse
# the per-turn music-box chime must NOT play during a finale build
assert html.count("<audio") == 1
def test_build_keeps_the_child_visible(self):
html = render_entity(50, "build", seq=4)
assert "entity-img" in html
class TestConvulsionModes:
def test_convulse_good_is_gentle_and_silent(self):
html = render_entity(80, "convulse_good", seq=2)
assert "entity-convulse-soft" in html # soft animation wrapper
assert "entity-rage-tint" not in html # no blood-red tint
assert "entity-settle-face" in html # eases into the smile
# silent: the sigh plays on the settle, not during the convulsion
assert "<audio" not in html
def test_convulse_loop_is_fast_and_silent(self):
html = render_entity(80, "convulse_loop", seq=2)
assert "entity-frenzy-wrap" in html # reuses the hard convulse
assert "entity-convulse-loop" in html # faster 3-cycle variant
assert "entity-settle-face" in html # eases into the end face
# silent: the flatline plays on the settle
assert "<audio" not in html
class TestSettleModes:
def test_peace_settle_shows_the_smile_and_sighs(self):
html = render_entity(80, "peace_settle", seq=2)
assert "entity-peace" in html # the smile
assert "cue-audio" in html # the relief sigh (queued)
def test_end_settle_shows_the_end_face_and_flatlines(self):
html = render_entity(80, "end_settle", seq=2)
assert "entity-end" in html
assert "cue-audio" in html # the flatline (queued)