返回 CodeWhale
tests.rs
根目录 / crates / tui / src / tui / ambient_life / tests.rs
1 use super::*;
2 use ratatui::text::Span;
3
4 #[test]
5 fn ambient_min_dimensions_allow_small_windows() {
6 const {
7 assert!(AMBIENT_MIN_WIDTH < 68);
8 assert!(AMBIENT_MIN_HEIGHT < 15);
9 }
10 }
11
12 #[test]
13 fn occupied_text_bounds_skips_string_join() {
14 let line = Line::from(vec![Span::raw(" hello "), Span::raw("world ")]);
15 let (start, end) = occupied_text_bounds(&line).expect("bounds");
16 assert_eq!(start, 2);
17 assert!(end > start);
18 }
19
20 #[test]
21 fn whale_cameo_is_brief() {
22 assert_eq!(whale_cameo_phase(0), WhaleCameoPhase::Breach);
23 assert_eq!(whale_cameo_phase(500), WhaleCameoPhase::Spout);
24 assert_eq!(whale_cameo_phase(1_200), WhaleCameoPhase::Fluke);
25 assert_eq!(whale_cameo_phase(2_000), WhaleCameoPhase::Submerge);
26 assert_eq!(whale_cameo_phase(3_000), WhaleCameoPhase::Hidden);
27 }
28
29 #[test]
30 fn density_scales_with_area() {
31 assert_eq!(
32 LifeDensity::from_area(Rect::new(0, 0, 40, 10)),
33 LifeDensity::Sparse
34 );
35 assert_eq!(
36 LifeDensity::from_area(Rect::new(0, 0, 100, 30)),
37 LifeDensity::Rich
38 );
39 }
40
41 #[test]
42 fn fish_school_uses_one_silhouette_family() {
43 // Never mix lone `>` with full fish bodies; the lead just gains an
44 // eye within the same family.
45 assert_eq!(fish_body(true, false), "><>");
46 assert_eq!(fish_body(false, false), "<><");
47 assert_eq!(fish_body(true, true), "><o>");
48 assert_eq!(fish_body(false, true), "<o><");
49 }
50
51 fn frame_at(t: u128) -> FrameMarks {
52 let area = Rect::new(0, 0, 100, 30);
53 let mut stats = AmbientFrameStats::default();
54 build_frame_marks(
55 area,
56 t,
57 LifeDensity::from_area(area),
58 AmbientCursor::default(),
59 WhaleCameo::default(),
60 &mut stats,
61 )
62 }
63
64 #[test]
65 fn fish_always_swim_the_way_they_face() {
66 // Wrap-around construction: within one crossing, a right-facing
67 // school only ever moves right, and a left-facing school only left.
68 let area_travel = 100u128 + 21; // width + wedge span (see constants)
69 let cycle_ms = area_travel * SCHOOL_CELL_MS;
70 for cycle in 0u128..6 {
71 // The school clock carries a half-cycle head start, so sampling
72 // at cycle*cycle_ms lands mid-crossing of `cycle`.
73 let t1 = cycle * cycle_ms;
74 let t2 = t1 + SCHOOL_CELL_MS * 3;
75 let lead = |t: u128| {
76 frame_at(t)
77 .marks
78 .into_iter()
79 .find(|mark| mark.glyph.contains('o'))
80 };
81 let (Some(a), Some(b)) = (lead(t1), lead(t2)) else {
82 continue; // school off-screen at this sample — fine
83 };
84 let expect_right = school_swims_right(cycle);
85 if expect_right {
86 assert_eq!(a.glyph, "><o>", "cycle {cycle} facing");
87 assert!(b.x >= a.x, "cycle {cycle}: right-facing fish moved left");
88 } else {
89 assert_eq!(a.glyph, "<o><", "cycle {cycle} facing");
90 assert!(b.x <= a.x, "cycle {cycle}: left-facing fish moved right");
91 }
92 }
93 // Both directions must actually occur across nearby cycles.
94 let dirs: Vec<bool> = (0u128..12).map(school_swims_right).collect();
95 assert!(
96 dirs.iter().any(|d| *d) && dirs.iter().any(|d| !*d),
97 "{dirs:?}"
98 );
99 }
100
101 #[test]
102 fn water_holds_only_fish_bubbles_and_jellyfish() {
103 // Seaweed and bio-dust are gone (2026-07-23): every mark is a fish
104 // body, a bubble glyph, or a jellyfish part.
105 for t in [0u128, 7_500, 33_000, 61_000, 120_000] {
106 for mark in frame_at(t).marks {
107 let ok = matches!(mark.glyph, "><>" | "<><" | "><o>" | "<o><")
108 || matches!(mark.glyph, "·" | "˚" | "°")
109 || JELLY_DOME_TOP_FRAMES.contains(&mark.glyph)
110 || JELLY_DOME_SKIRT_FRAMES.contains(&mark.glyph)
111 || JELLY_DOME_TOP_COMPACT.contains(&mark.glyph)
112 || JELLY_DOME_SKIRT_COMPACT.contains(&mark.glyph)
113 || JELLY_TENTACLE_FRAMES.contains(&mark.glyph);
114 assert!(ok, "unexpected ambient glyph {:?} at t={t}", mark.glyph);
115 }
116 }
117 }
118
119 #[test]
120 fn ambient_glyphs_are_ascii_or_have_fallbacks() {
121 // Every glyph the water can paint is either pure ASCII (fish and
122 // the whole jellyfish silhouette, by construction) or carries a
123 // glyphs::ascii_fallback entry (bubbles, whale cameo) so
124 // CODEWHALE_ASCII_SAFE=1 covers the whole field.
125 let mut jellyfish: Vec<&str> = Vec::new();
126 jellyfish.extend(JELLY_DOME_TOP_FRAMES);
127 jellyfish.extend(JELLY_DOME_SKIRT_FRAMES);
128 jellyfish.extend(JELLY_DOME_TOP_COMPACT);
129 jellyfish.extend(JELLY_DOME_SKIRT_COMPACT);
130 jellyfish.extend(JELLY_TENTACLE_FRAMES);
131 for glyph in jellyfish {
132 assert!(glyph.is_ascii(), "jellyfish glyph {glyph:?} must be ASCII");
133 }
134 for glyph in ["><>", "<><", "><o>", "<o><"] {
135 assert!(glyph.is_ascii(), "fish glyph {glyph:?} must be ASCII");
136 }
137 for glyph in ["·", "˚", "°", "≈≈>", "≈", "~"] {
138 assert!(
139 glyph.is_ascii() || crate::tui::glyphs::ascii_fallback(glyph).is_some(),
140 "ambient glyph {glyph:?} lacks an ASCII fallback"
141 );
142 }
143 }
144
145 #[test]
146 fn jellyfish_reads_as_dome_with_lagging_tentacles() {
147 // Find a frame where a full jelly is on-screen and assert its
148 // structure: a two-row dome at least four cells wide, exactly three
149 // tentacle columns one row below the skirt, and both dome and
150 // tentacles holding their brightness floors.
151 let mut seen = false;
152 for probe in 0..240u128 {
153 let t = probe * 500;
154 let frame = frame_at(t);
155 let Some(top) = frame
156 .marks
157 .iter()
158 .find(|mark| JELLY_DOME_TOP_FRAMES.contains(&mark.glyph))
159 else {
160 continue;
161 };
162 let dome_w = UnicodeWidthStr::width(top.glyph) as u16;
163 assert!(dome_w >= 4, "rich dome too narrow: {:?}", top.glyph);
164 let Some(skirt) = frame.marks.iter().find(|mark| {
165 JELLY_DOME_SKIRT_FRAMES.contains(&mark.glyph) && mark.x == top.x && mark.y == top.y + 1
166 }) else {
167 panic!("visible jellyfish dome lost its skirt: {frame:?}");
168 };
169 let tentacles: Vec<&AmbientMark> = frame
170 .marks
171 .iter()
172 .filter(|mark| {
173 JELLY_TENTACLE_FRAMES.contains(&mark.glyph)
174 && mark.y == skirt.y + 1
175 && mark.x > top.x
176 && mark.x < top.x + dome_w
177 })
178 .collect();
179 assert_eq!(
180 tentacles.len(),
181 3,
182 "visible jellyfish must keep all tentacles: {frame:?}"
183 );
184 let dome_glow = top.brightness.expect("dome pulses");
185 assert!(dome_glow >= JELLY_BRIGHTNESS_FLOOR - f32::EPSILON);
186 for tentacle in &tentacles {
187 let glow = tentacle.brightness.expect("tentacle pulses");
188 assert!(glow >= JELLY_BRIGHTNESS_FLOOR - f32::EPSILON);
189 }
190 seen = true;
191 break;
192 }
193 assert!(seen, "no complete jellyfish found in 120s of frames");
194 }
195
196 #[test]
197 fn jellyfish_tentacles_sway_out_of_phase_and_dome_pulses() {
198 // Over a sweep: the dome shows both pulse frames within a few
199 // JELLY_PULSE_MS periods, each tentacle column cycles its sway
200 // frames, and the trio is not always in lockstep (per-column phase
201 // offset — the lag is what sells "jellyfish").
202 let mut dome_frames = std::collections::BTreeSet::new();
203 let mut column_frames: [std::collections::BTreeSet<&str>; 3] = Default::default();
204 let mut saw_desync = false;
205 for probe in 0..480u128 {
206 let frame = frame_at(probe * 100);
207 let Some(top) = frame
208 .marks
209 .iter()
210 .find(|mark| JELLY_DOME_TOP_FRAMES.contains(&mark.glyph))
211 else {
212 continue;
213 };
214 dome_frames.insert(top.glyph);
215 let mut trio = [""; 3];
216 let mut found = 0usize;
217 for (col, slot) in trio.iter_mut().enumerate() {
218 if let Some(tentacle) = frame.marks.iter().find(|mark| {
219 JELLY_TENTACLE_FRAMES.contains(&mark.glyph)
220 && mark.y == top.y + 2
221 && mark.x == top.x + 1 + col as u16
222 }) {
223 *slot = tentacle.glyph;
224 column_frames[col].insert(tentacle.glyph);
225 found += 1;
226 }
227 }
228 if found == 3 && !(trio[0] == trio[1] && trio[1] == trio[2]) {
229 saw_desync = true;
230 }
231 }
232 assert_eq!(
233 dome_frames.len(),
234 JELLY_DOME_TOP_FRAMES.len(),
235 "dome pulse should show every frame: {dome_frames:?}"
236 );
237 for (col, set) in column_frames.iter().enumerate() {
238 assert!(set.len() > 1, "tentacle column {col} never swayed");
239 }
240 assert!(saw_desync, "tentacle columns strobed in lockstep");
241 }
242
243 #[test]
244 fn sparse_water_gets_a_compact_jellyfish() {
245 // Narrow fallback: the Sparse tier swaps the full 5-cell dome for a
246 // 3-cell one with two tentacles — a different silhouette, not just
247 // fewer jellies.
248 let area = Rect::new(0, 0, 48, 12);
249 let mut saw_compact = false;
250 let mut saw_full = false;
251 let mut saw_two_tentacles = false;
252 for probe in 0..240u128 {
253 let mut stats = AmbientFrameStats::default();
254 let frame = build_frame_marks(
255 area,
256 probe * 500,
257 LifeDensity::from_area(area),
258 AmbientCursor::default(),
259 WhaleCameo::default(),
260 &mut stats,
261 );
262 for mark in &frame.marks {
263 saw_compact |= JELLY_DOME_TOP_COMPACT.contains(&mark.glyph);
264 saw_full |= JELLY_DOME_TOP_FRAMES.contains(&mark.glyph);
265 }
266 let Some(top) = frame
267 .marks
268 .iter()
269 .find(|mark| JELLY_DOME_TOP_COMPACT.contains(&mark.glyph))
270 else {
271 continue;
272 };
273 let tentacles = frame
274 .marks
275 .iter()
276 .filter(|mark| JELLY_TENTACLE_FRAMES.contains(&mark.glyph) && mark.y == top.y + 2)
277 .count();
278 assert_eq!(
279 tentacles, 2,
280 "visible compact jelly must keep both tentacles: {frame:?}"
281 );
282 saw_two_tentacles = true;
283 }
284 assert!(saw_compact, "sparse water never showed the compact dome");
285 assert!(!saw_full, "sparse water used the full-size dome");
286 assert!(saw_two_tentacles, "compact jelly lost its tentacles");
287 }
288
289 #[test]
290 fn animated_tentacle_frames_never_collapse_to_punctuation_dots() {
291 assert!(
292 JELLY_TENTACLE_FRAMES
293 .iter()
294 .all(|glyph| matches!(*glyph, "|" | "/" | "\\")),
295 "every animation frame must retain a legible tentacle stroke"
296 );
297 }
298
299 #[test]
300 fn motion_is_a_deterministic_function_of_elapsed_time() {
301 // v0.9.4: positions always ride the monotonic clock (presence fades the
302 // marks in/out instead of parking them), so a fixed t must produce the
303 // exact same complete jellyfish: dome, skirt, and tentacles visible.
304 let area = Rect::new(0, 0, 100, 30);
305 let build = || {
306 let mut stats = AmbientFrameStats::default();
307 build_frame_marks(
308 area,
309 0,
310 LifeDensity::from_area(area),
311 AmbientCursor::default(),
312 WhaleCameo::default(),
313 &mut stats,
314 )
315 };
316 let first = build();
317 let second = build();
318 let pose = |frame: &FrameMarks| {
319 frame
320 .marks
321 .iter()
322 .map(|mark| (mark.glyph, mark.x, mark.y))
323 .collect::<Vec<_>>()
324 };
325 assert_eq!(
326 pose(&first),
327 pose(&second),
328 "motion must be a pure function of t"
329 );
330 let top = first
331 .marks
332 .iter()
333 .find(|mark| JELLY_DOME_TOP_FRAMES.contains(&mark.glyph))
334 .expect("jellyfish dome at t=0");
335 assert!(
336 first
337 .marks
338 .iter()
339 .any(|mark| { JELLY_DOME_SKIRT_FRAMES.contains(&mark.glyph) && mark.y == top.y + 1 }),
340 "jellyfish lost its skirt at t=0"
341 );
342 let tentacles = first
343 .marks
344 .iter()
345 .filter(|mark| JELLY_TENTACLE_FRAMES.contains(&mark.glyph) && mark.y == top.y + 2)
346 .count();
347 assert!(tentacles >= 3, "jellyfish lost its tentacles at t=0");
348 }
349
350 #[test]
351 fn glow_helpers_stay_bounded_with_floors() {
352 for t in (0u128..12_000).step_by(97) {
353 let w = wave01(t, FISH_WAVE_MS, 0);
354 assert!((0.0..=1.0).contains(&w), "wave01 out of range: {w}");
355 let g = glint01(t, 2_600, 600, BUBBLE_BRIGHTNESS_FLOOR, 0);
356 assert!(
357 (BUBBLE_BRIGHTNESS_FLOOR..=1.0).contains(&g),
358 "glint01 lost its floor: {g}"
359 );
360 }
361 }
362
363 #[test]
364 fn frame_stats_account_for_every_mark() {
365 let area = Rect::new(0, 0, 100, 30);
366 let mut buf = Buffer::empty(area);
367 let stats = render_ambient_life(
368 area,
369 &mut buf,
370 (Color::Cyan, Color::Blue),
371 &[],
372 12_000,
373 1.0,
374 AmbientCursor::default(),
375 WhaleCameo::default(),
376 );
377 assert_eq!(
378 stats.marks_built,
379 stats.marks_painted + stats.marks_skipped_text + stats.marks_clipped,
380 "every built mark is painted, text-skipped, or clipped: {stats:?}"
381 );
382 assert!(stats.marks_painted > 0, "empty water should paint life");
383 // cells_written counts each cell of a multi-cell glyph (and counts
384 // a shared cell once per overlapping mark), so the honest bound is
385 // per painted mark — at most the widest glyph (the 5-cell dome) —
386 // rather than per area cell.
387 assert!(stats.cells_written >= stats.marks_painted);
388 assert!(
389 stats.cells_written <= stats.marks_painted * 5,
390 "cells_written out of proportion: {stats:?}"
391 );
392 }
393
394 #[test]
395 fn frame_stats_stay_within_the_render_budget() {
396 // Worst case on the largest Rich field with the whale cameo active:
397 // 7 fish + 2 jellies x 5 parts + 2 bubbles + 2 cameo cells. Anything
398 // more is a leak in the O(1)-per-frame budget.
399 let area = Rect::new(0, 0, 160, 40);
400 let mut buf = Buffer::empty(area);
401 let whale = WhaleCameo {
402 elapsed_ms: Some(500), // Spout: cameo glyph plus spray
403 anchor_x: 80,
404 anchor_y: 26,
405 };
406 let stats = render_ambient_life(
407 area,
408 &mut buf,
409 (Color::Cyan, Color::Blue),
410 &[],
411 12_000,
412 1.0,
413 AmbientCursor::default(),
414 whale,
415 );
416 assert!(
417 stats.marks_built <= MAX_FRAME_MARKS,
418 "frame budget blown: {stats:?}"
419 );
420 assert_eq!(
421 stats.marks_built,
422 stats.marks_painted + stats.marks_skipped_text + stats.marks_clipped,
423 "every built mark is accounted for: {stats:?}"
424 );
425 }
426
427 #[test]
428 fn frame_stats_never_overwrite_text() {
429 // Property: on a text-covered field, colliding marks are charged to
430 // marks_skipped_text and no transcript cell is overwritten.
431 let area = Rect::new(0, 0, 100, 30);
432 let mut buf = Buffer::empty(area);
433 let lines: Vec<Line<'static>> = (0..usize::from(area.height))
434 .map(|i| {
435 Line::from(Span::raw(format!(
436 "transcript row {i:02} occupies the water"
437 )))
438 })
439 .collect();
440 for (i, line) in lines.iter().enumerate() {
441 buf.set_line(area.x, area.y + i as u16, line, area.width);
442 }
443 let stats = render_ambient_life(
444 area,
445 &mut buf,
446 (Color::Cyan, Color::Blue),
447 &lines,
448 12_000,
449 1.0,
450 AmbientCursor::default(),
451 WhaleCameo::default(),
452 );
453 assert!(
454 stats.marks_skipped_text > 0,
455 "text-covered water should skip some marks: {stats:?}"
456 );
457 assert_eq!(
458 stats.marks_built,
459 stats.marks_painted + stats.marks_skipped_text + stats.marks_clipped,
460 "every built mark is accounted for: {stats:?}"
461 );
462 for (i, line) in lines.iter().enumerate() {
463 let text: String = line
464 .spans
465 .iter()
466 .map(|span| span.content.as_ref())
467 .collect();
468 for (x, ch) in text.chars().enumerate() {
469 assert_eq!(
470 buf[(area.x + x as u16, area.y + i as u16)].symbol(),
471 ch.to_string(),
472 "ambient life overwrote transcript cell ({x},{i})"
473 );
474 }
475 }
476 }
477
478 fn full_jellyfish_frame(x: u16, y: u16) -> FrameMarks {
479 let mark = |x, y, glyph, depth| AmbientMark {
480 x,
481 y,
482 glyph,
483 jellyfish: Some(0),
484 depth,
485 style_mod: None,
486 brightness: None,
487 };
488 FrameMarks {
489 marks: vec![
490 mark(x, y, JELLY_DOME_TOP_FRAMES[0], Depth::Midground),
491 mark(x, y + 1, JELLY_DOME_SKIRT_FRAMES[0], Depth::Midground),
492 mark(x + 1, y + 2, "|", Depth::Background),
493 mark(x + 2, y + 2, "/", Depth::Background),
494 mark(x + 3, y + 2, "\\", Depth::Background),
495 ],
496 }
497 }
498
499 fn paint_fixture(
500 area: Rect,
501 lines: &[Line<'static>],
502 frame: &FrameMarks,
503 ) -> (Buffer, AmbientFrameStats) {
504 let mut buf = Buffer::empty(area);
505 for (row, line) in lines.iter().enumerate() {
506 buf.set_line(area.x, area.y + row as u16, line, area.width);
507 }
508 let mut stats = AmbientFrameStats {
509 marks_built: frame.marks.len() as u32,
510 ..AmbientFrameStats::default()
511 };
512 paint_marks(
513 area,
514 &mut buf,
515 (Color::Cyan, Color::Blue),
516 lines,
517 frame,
518 1.0,
519 &mut stats,
520 );
521 (buf, stats)
522 }
523
524 #[test]
525 fn jellyfish_relocates_as_one_nearest_silhouette() {
526 let area = Rect::new(0, 0, 24, 8);
527 let mut lines = vec![Line::default(); usize::from(area.height)];
528 lines[2] = Line::from(Span::raw(" X"));
529 let (buf, stats) = paint_fixture(area, &lines, &full_jellyfish_frame(10, 2));
530
531 assert_eq!(buf[(10, 2)].symbol(), "X");
532 assert_eq!(buf[(12, 2)].symbol(), ".");
533 assert_eq!(buf[(12, 3)].symbol(), "\\");
534 assert_eq!(buf[(13, 4)].symbol(), "|");
535 assert_eq!(buf[(14, 4)].symbol(), "/");
536 assert_eq!(buf[(15, 4)].symbol(), "\\");
537 assert_eq!(stats.marks_painted, 5);
538 assert_eq!(stats.marks_skipped_text, 0);
539 assert_eq!(stats.marks_clipped, 0);
540 assert_eq!(stats.cells_written, 13);
541 }
542
543 #[test]
544 fn jellyfish_hides_rather_than_vaulting_past_a_long_line() {
545 // This used to place the silhouette at x=33 — a 17-column vault from its
546 // lane, chosen purely from the width of the text on that row. Under a
547 // fast stream that width changes every frame, so the vault was the
548 // teleport users saw. Past the dodge cap the jelly is withheld instead,
549 // which is the same quiet outcome the fish already have.
550 let area = Rect::new(0, 0, 100, 8);
551 let mut lines = vec![Line::default(); usize::from(area.height)];
552 lines[2] = Line::from(Span::raw("X".repeat(32)));
553 let (_, stats) = paint_fixture(area, &lines, &full_jellyfish_frame(16, 2));
554
555 assert_eq!(stats.marks_painted, 0);
556 assert_eq!(stats.marks_skipped_text, 5);
557 assert_eq!(stats.marks_clipped, 0);
558 }
559
560 #[test]
561 fn jellyfish_never_teleports_while_a_line_streams_across_its_lane() {
562 // Regression for the DeepSeek-V4-Flash report: replay a line growing
563 // straight through the silhouette's lane and assert the painted anchor
564 // never jumps. The clock is held fixed, so the only moving input is the
565 // transcript text — the coupling that actually scaled with token
566 // throughput.
567 //
568 // The burst size is the whole point. A slow provider adds about a
569 // character per frame and the old unbounded dodge slid along with it,
570 // which is why this never looked broken on slow models. A fast one adds a
571 // burst per frame, and the dodge moved by the whole burst at once.
572 const STREAM_BURST_CHARS: usize = 9;
573 let area = Rect::new(0, 0, 100, 8);
574 let lane = 40u16;
575 let mut previous: Option<u16> = None;
576 for chars in (0..80usize).step_by(STREAM_BURST_CHARS) {
577 let mut lines = vec![Line::default(); usize::from(area.height)];
578 lines[2] = Line::from(Span::raw("X".repeat(chars)));
579 lines[3] = Line::from(Span::raw("X".repeat(chars.saturating_sub(3))));
580 let (buf, _) = paint_fixture(area, &lines, &full_jellyfish_frame(lane, 2));
581 let anchor = (0..area.width).find(|x| buf[(*x, 2)].symbol() == ".");
582 if let (Some(anchor), Some(previous)) = (anchor, previous) {
583 assert!(
584 anchor.abs_diff(previous) <= 2 * JELLY_MAX_TEXT_DODGE_COLS,
585 "jellyfish teleported {} columns at {chars} streamed chars",
586 anchor.abs_diff(previous)
587 );
588 }
589 if let Some(anchor) = anchor {
590 assert!(
591 anchor.abs_diff(lane) <= JELLY_MAX_TEXT_DODGE_COLS,
592 "jellyfish left its lane by {} columns",
593 anchor.abs_diff(lane)
594 );
595 }
596 previous = anchor;
597 }
598 }
599
600 #[test]
601 fn sparse_jellyfish_row_keeps_a_safe_gap_over_text() {
602 let area = Rect::new(0, 0, 12, 6);
603 let mut lines = vec![Line::default(); usize::from(area.height)];
604 lines[2] = Line::from(Span::raw(" X"));
605 let mark = |x| AmbientMark {
606 x,
607 y: 2,
608 glyph: "|",
609 jellyfish: Some(0),
610 depth: Depth::Background,
611 style_mod: None,
612 brightness: None,
613 };
614 let frame = FrameMarks {
615 marks: vec![mark(0), mark(6)],
616 };
617 let (buf, stats) = paint_fixture(area, &lines, &frame);
618
619 assert_eq!(buf[(0, 2)].symbol(), "|");
620 assert_eq!(buf[(3, 2)].symbol(), "X");
621 assert_eq!(buf[(6, 2)].symbol(), "|");
622 assert_eq!(stats.marks_painted, 2);
623 }
624
625 #[test]
626 fn fully_blocked_jellyfish_is_suppressed_and_accounted() {
627 let area = Rect::new(0, 0, 24, 8);
628 let mut lines = vec![Line::default(); usize::from(area.height)];
629 for line in lines.iter_mut().take(5).skip(2) {
630 *line = Line::from(Span::raw("X".repeat(24)));
631 }
632 let (buf, stats) = paint_fixture(area, &lines, &full_jellyfish_frame(10, 2));
633
634 assert_eq!(stats.marks_painted, 0);
635 assert_eq!(stats.marks_skipped_text, 5);
636 assert_eq!(stats.marks_clipped, 0);
637 assert_eq!(stats.cells_written, 0);
638 assert_eq!(stats.marks_built, stats.marks_skipped_text);
639 for row in 2..=4 {
640 for column in 0..area.width {
641 assert_eq!(buf[(column, row)].symbol(), "X");
642 }
643 }
644 }
645
646 #[test]
647 fn too_wide_jellyfish_is_truthfully_clipped_as_a_group() {
648 let area = Rect::new(0, 0, 4, 8);
649 let lines = vec![Line::default(); usize::from(area.height)];
650 let (_, stats) = paint_fixture(area, &lines, &full_jellyfish_frame(0, 2));
651
652 assert_eq!(stats.marks_painted, 0);
653 assert_eq!(stats.marks_skipped_text, 0);
654 assert_eq!(stats.marks_clipped, 5);
655 assert_eq!(stats.marks_built, stats.marks_clipped);
656 }
657
657 lines RUST