use ai_app::ui::fixture::{PHONE_FRAME_MS, PHONE_SCALE, phone_size}; use iris::harness::{Harness, TouchAction}; use iris::prelude::*; fn fence_scroll_in(h: &Harness, top: f32, bottom: f32) -> Option<(WidgetId, PixelRegion)> { let render_handle = h.rsc.ui.render_state(); let render_state = render_handle.get(); render_state .active .keys() .copied() .filter(|&id| { h.rsc .ui .widgets .get_dyn(id) .and_then(|w| w.as_any().downcast_ref::()) .is_some_and(|s| s.axis() == Axis::X) }) .find_map(|id| { let r = render_state.window_region(&id, &h.rsc)?; (r.top_left.y >= top && r.bot_right.y <= bottom).then_some((id, r)) }) } fn is_scrolling(h: &Harness, id: WidgetId) -> bool { h.rsc .ui .widgets .get_dyn(id) .and_then(|w| w.as_any().downcast_ref::()) .expect("the fence's scroll area is still drawn") .is_scrolling() } fn amt(h: &Harness, id: WidgetId) -> f32 { h.rsc .ui .widgets .get_dyn(id) .and_then(|w| w.as_any().downcast_ref::()) .expect("the fence's scroll area is still drawn") .amt() } #[test] fn a_flick_across_a_code_fence_keeps_moving_after_the_finger_leaves() { use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow}; let mut h = Harness::new(phone_size(), PHONE_SCALE); let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds"); let mut screen = opened.screen; h.frame(0); h.frame(PHONE_FRAME_MS); let fence = TranscriptRow::Single(TranscriptItem::AssistantMsg { seq: 9_000_000, text: "```\none two three four five six seven eight nine ten eleven twelve \ thirteen fourteen fifteen sixteen seventeen eighteen twenty twentyone\n```" .to_string(), settled: true, }); screen.push_row(&mut h.rsc, &fence); (screen.list)(&mut h.rsc).jump_to_end(); h.frame(100); h.frame(108); let key = ai_app::ui::row::row_key(&fence.key()); let (top, bottom) = (screen.list)(&mut h.rsc) .extent(key) .expect("the fence row is on screen"); let (fence_scroll, box_) = fence_scroll_in(&h, top, bottom) .expect("the pushed fence draws a horizontal scroll area of its own"); assert_eq!(amt(&h, fence_scroll), 0.0, "a fence opens at its start"); let y = (box_.top_left.y + box_.bot_right.y) / 2.0; h.touch(TouchAction::Down, Vec2::new(900.0, y), 200); for (i, x) in [860.0, 800.0, 720.0, 620.0].into_iter().enumerate() { h.touch(TouchAction::Move, Vec2::new(x, y), 208 + 8 * i as u64); } h.touch(TouchAction::Up, Vec2::new(620.0, y), 240); let at_release = amt(&h, fence_scroll); assert!( at_release > 0.0, "the flick itself must have panned the fence, got {at_release}" ); let mut t = 240; while t <= 740 { h.frame(t); t += PHONE_FRAME_MS; } let coasted = amt(&h, fence_scroll); assert!( coasted > at_release + 1.0, "the fence stopped dead at the release: {at_release} -> {coasted}" ); let settled = coasted; while t <= 4_000 { h.frame(t); t += PHONE_FRAME_MS; } let after = amt(&h, fence_scroll); assert!( after >= settled, "a fling must not run backwards: {settled} -> {after}" ); let last = after; h.frame(t); assert_eq!(last, amt(&h, fence_scroll), "the fling never settled"); } #[test] fn a_drag_away_from_a_coasting_fence_scrolls_the_list_and_leaves_it_coasting() { use ai_app::client::transcript_fold::{TranscriptItem, TranscriptRow}; let mut h = Harness::new(phone_size(), PHONE_SCALE); let opened = ai_app::ui::fixture::open(&mut h.rsc, &mut h.state).expect("the fixture folds"); let mut screen = opened.screen; h.frame(0); h.frame(PHONE_FRAME_MS); let fence = TranscriptRow::Single(TranscriptItem::AssistantMsg { seq: 9_000_000, text: format!( "```\n{}\n```", (1..=200) .map(|i| format!("word{i}")) .collect::>() .join(" ") ), settled: true, }); screen.push_row(&mut h.rsc, &fence); (screen.list)(&mut h.rsc).jump_to_end(); h.frame(100); h.frame(108); let key = ai_app::ui::row::row_key(&fence.key()); let (top, bottom) = (screen.list)(&mut h.rsc) .extent(key) .expect("the fence row is on screen"); let (fence_scroll, box_) = fence_scroll_in(&h, top, bottom) .expect("the pushed fence draws a horizontal scroll area of its own"); let y = (box_.top_left.y + box_.bot_right.y) / 2.0; h.touch(TouchAction::Down, Vec2::new(900.0, y), 200); for (i, x) in [860.0, 800.0, 720.0, 620.0].into_iter().enumerate() { h.touch(TouchAction::Move, Vec2::new(x, y), 208 + 8 * i as u64); } h.touch(TouchAction::Up, Vec2::new(620.0, y), 240); h.frame(248); assert!( is_scrolling(&h, fence_scroll), "the fence has to still be coasting for this to be the reported case", ); let probe = box_.top_left.y - 500.0; let row = (screen.list)(&mut h.rsc) .key_at(probe) .expect("a row that far up the screen"); let (row_top, row_bottom) = (screen.list)(&mut h.rsc).extent(row).expect("its extent"); let from = (row_top + row_bottom) / 2.0; let list_before = (screen.list)(&mut h.rsc).anchor_position_display(); let fence_before = amt(&h, fence_scroll); h.touch(TouchAction::Down, Vec2::new(540.0, from), 256); let mut t = 264; for i in 1..=8 { h.touch( TouchAction::Move, Vec2::new(540.0, from + 20.0 * i as f32), t, ); t += 8; } h.touch(TouchAction::Up, Vec2::new(540.0, from + 160.0), t); assert_ne!( list_before, (screen.list)(&mut h.rsc).anchor_position_display(), "the drag was nowhere near the fence, so it belongs to the list", ); assert!( amt(&h, fence_scroll) > fence_before, "the fence's fling must carry on through a gesture that was never \ its own: {fence_before} -> {}", amt(&h, fence_scroll), ); }