Spaces:
Sleeping
Sleeping
File size: 14,956 Bytes
03129db 5154bf5 673925d c30144e 6226702 5154bf5 a887da3 5154bf5 a887da3 5154bf5 03129db 5154bf5 673925d 34c2d24 673925d 0ed95ef c30144e 0ed95ef c30144e 0ed95ef 0bbde8d 0ed95ef 41c845f 64b3043 0ed95ef 0bbde8d 0ed95ef 0bbde8d 0ed95ef 64b3043 0ed95ef c30144e 0ed95ef 9b82354 0ed95ef 9b82354 0ed95ef 6790748 2822e77 915fdd8 0bbde8d 2822e77 915fdd8 41c845f 915fdd8 c30144e 2822e77 fae2f18 5154bf5 fae2f18 d207901 0bbde8d d54023d d207901 6854db6 0e68c4b 2e78e50 0e68c4b 2e78e50 0e68c4b 2e78e50 85d27fb 626f99c c116919 626f99c d54023d 626f99c 85d27fb da6f3c4 c7e26db 98232c3 c7e26db 98232c3 c7e26db 98232c3 c7e26db 98232c3 85d27fb 98232c3 85d27fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | 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 "<script>" in out
def test_ampersand_is_escaped(self):
out = render_treasure(["mom & dad"])
assert "mom & 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 "<b>" 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)
|