1use crate::TestHarness;
2use anyhow::{anyhow, Result};
3use fission_core::action::GlobalState;
4use fission_core::event::{ImeEvent, InputEvent, KeyCode, KeyEvent, PointerButton, PointerEvent};
5use fission_ir::{LayoutOp, Op, WidgetId};
6use fission_layout::{LayoutPoint, LayoutRect, LayoutSize};
7use fission_render::DisplayOp;
8
9#[derive(Debug, Clone)]
10pub struct TextMatch {
11 pub text: String,
12 pub bounds: LayoutRect,
13 pub node_id: Option<WidgetId>,
14}
15
16#[derive(Debug, Clone)]
17pub struct SemanticMatch {
18 pub role: fission_ir::semantics::Role,
19 pub label: Option<String>,
20 pub bounds: LayoutRect,
21 pub node_id: WidgetId,
22}
23
24pub struct TestDriver<S: GlobalState> {
25 pub harness: TestHarness<S>,
26 auto_pump: bool,
27}
28
29impl<S: GlobalState> TestDriver<S> {
30 pub fn new(harness: TestHarness<S>) -> Self {
31 Self {
32 harness,
33 auto_pump: true,
34 }
35 }
36
37 pub fn set_viewport(&mut self, width: f32, height: f32) {
38 self.harness.env.viewport_size = LayoutSize::new(width, height);
39 }
40
41 pub fn pump(&mut self) -> Result<()> {
42 self.harness.pump()
43 }
44
45 pub fn find_text(&self, needle: &str) -> Option<TextMatch> {
48 self.find_all_text(needle).into_iter().next()
49 }
50
51 pub fn find_all_text(&self, needle: &str) -> Vec<TextMatch> {
52 let dl = match self.harness.get_last_display_list() {
53 Some(dl) => dl,
54 None => return vec![],
55 };
56 let mut results = Vec::new();
57 for op in &dl.ops {
58 match op {
59 DisplayOp::DrawText {
60 text,
61 bounds,
62 node_id,
63 ..
64 } => {
65 if text.contains(needle) {
66 results.push(TextMatch {
67 text: text.clone(),
68 bounds: *bounds,
69 node_id: *node_id,
70 });
71 }
72 }
73 DisplayOp::DrawRichText {
74 runs,
75 bounds,
76 node_id,
77 ..
78 } => {
79 let combined: String = runs.iter().map(|r| r.text.clone()).collect();
80 if combined.contains(needle) {
81 results.push(TextMatch {
82 text: combined,
83 bounds: *bounds,
84 node_id: *node_id,
85 });
86 }
87 }
88 _ => {}
89 }
90 }
91 results
92 }
93
94 pub fn find_role(&self, role: fission_ir::semantics::Role) -> Vec<SemanticMatch> {
95 let ir = match &self.harness.last_ir {
96 Some(ir) => ir,
97 None => return vec![],
98 };
99 let snapshot = match &self.harness.last_snapshot {
100 Some(s) => s,
101 None => return vec![],
102 };
103 let mut results = Vec::new();
104 for (id, node) in &ir.nodes {
105 if let Op::Semantics(sem) = &node.op {
106 if sem.role == role {
107 let bounds = snapshot
108 .get_node_rect(*id)
109 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
110 results.push(SemanticMatch {
111 role: sem.role,
112 label: sem.label.clone(),
113 bounds,
114 node_id: *id,
115 });
116 }
117 }
118 }
119 results
120 }
121
122 pub fn get_all_visible_text(&self) -> Vec<String> {
123 let dl = match self.harness.get_last_display_list() {
124 Some(dl) => dl,
125 None => return vec![],
126 };
127 let mut texts = Vec::new();
128 for op in &dl.ops {
129 match op {
130 DisplayOp::DrawText { text, .. } => texts.push(text.clone()),
131 DisplayOp::DrawRichText { runs, .. } => {
132 texts.push(runs.iter().map(|r| r.text.clone()).collect());
133 }
134 _ => {}
135 }
136 }
137 texts
138 }
139
140 pub fn tap_point(&mut self, x: f32, y: f32) -> Result<()> {
143 let point = LayoutPoint::new(x, y);
144 self.harness
145 .send_event(InputEvent::Pointer(PointerEvent::Down {
146 point,
147 button: PointerButton::Primary,
148 modifiers: 0,
149 }))?;
150 self.harness
151 .send_event(InputEvent::Pointer(PointerEvent::Up {
152 point,
153 button: PointerButton::Primary,
154 modifiers: 0,
155 }))?;
156 if self.auto_pump {
157 self.harness.pump()?;
158 }
159 Ok(())
160 }
161
162 pub fn tap_text(&mut self, needle: &str) -> Result<()> {
163 let m = self
164 .find_text(needle)
165 .ok_or_else(|| anyhow!("text '{}' not found in display list", needle))?;
166 let cx = m.bounds.x() + m.bounds.width() / 2.0;
167 let cy = m.bounds.y() + m.bounds.height() / 2.0;
168 self.tap_point(cx, cy)
169 }
170
171 pub fn scroll(&mut self, at: LayoutPoint, delta: LayoutPoint) -> Result<()> {
172 self.harness
173 .send_event(InputEvent::Pointer(PointerEvent::Scroll {
174 point: at,
175 delta,
176 modifiers: 0,
177 }))?;
178 if self.auto_pump {
179 self.harness.pump()?;
180 }
181 Ok(())
182 }
183
184 pub fn scroll_down(&mut self, at: LayoutPoint, pixels: f32) -> Result<()> {
185 self.scroll(at, LayoutPoint::new(0.0, pixels))
186 }
187
188 pub fn scroll_to_text(&mut self, needle: &str) -> Result<()> {
189 let text_match = self
191 .find_text(needle)
192 .ok_or_else(|| anyhow!("text '{}' not found", needle))?;
193
194 let ir = self
196 .harness
197 .last_ir
198 .as_ref()
199 .ok_or_else(|| anyhow!("no IR"))?;
200 let snapshot = self
201 .harness
202 .last_snapshot
203 .as_ref()
204 .ok_or_else(|| anyhow!("no snapshot"))?;
205
206 let text_node_id = text_match
207 .node_id
208 .ok_or_else(|| anyhow!("text has no node_id"))?;
209
210 let mut current = ir.nodes.get(&text_node_id).and_then(|n| n.parent);
212 let mut scroll_id = None;
213 while let Some(pid) = current {
214 if let Some(pnode) = ir.nodes.get(&pid) {
215 if matches!(pnode.op, Op::Layout(LayoutOp::Scroll { .. })) {
216 scroll_id = Some(pid);
217 break;
218 }
219 current = pnode.parent;
220 } else {
221 break;
222 }
223 }
224
225 let scroll_id =
226 scroll_id.ok_or_else(|| anyhow!("no enclosing Scroll found for '{}'", needle))?;
227 let scroll_rect = snapshot
228 .get_node_rect(scroll_id)
229 .ok_or_else(|| anyhow!("scroll node has no rect"))?;
230
231 let text_y = text_match.bounds.y();
233 let viewport_top = scroll_rect.y();
234 let viewport_bottom = viewport_top + scroll_rect.height();
235
236 if text_y >= viewport_top && text_y + text_match.bounds.height() <= viewport_bottom {
237 return Ok(()); }
239
240 let needed_offset = (text_y - viewport_top - 20.0).max(0.0); self.harness
243 .runtime
244 .runtime_state
245 .scroll
246 .set_offset(scroll_id, needed_offset);
247 self.harness.pump()?;
248 Ok(())
249 }
250
251 pub fn type_text(&mut self, text: &str) -> Result<()> {
252 for ch in text.chars() {
253 if ch.is_ascii() {
254 let key_code = if ch == ' ' {
255 KeyCode::Space
256 } else if ch == '\n' {
257 KeyCode::Enter
258 } else {
259 KeyCode::Char(ch)
260 };
261 self.harness
262 .send_event(InputEvent::Keyboard(KeyEvent::Down {
263 key_code: key_code.clone(),
264 modifiers: 0,
265 }))?;
266 } else {
267 self.harness.send_event(InputEvent::Ime(ImeEvent::Commit {
269 text: ch.to_string(),
270 }))?;
271 }
272 }
273 if self.auto_pump {
274 self.harness.pump()?;
275 }
276 Ok(())
277 }
278
279 pub fn press_key(&mut self, key: KeyCode, modifiers: u8) -> Result<()> {
280 self.harness
281 .send_event(InputEvent::Keyboard(KeyEvent::Down {
282 key_code: key,
283 modifiers,
284 }))?;
285 if self.auto_pump {
286 self.harness.pump()?;
287 }
288 Ok(())
289 }
290
291 pub fn tick(&mut self, dt_ms: u64) -> Result<()> {
292 self.harness.tick(dt_ms)?;
293 if self.auto_pump {
294 self.harness.pump()?;
295 }
296 Ok(())
297 }
298
299 pub fn assert_text_visible(&self, needle: &str) {
302 assert!(
303 self.find_text(needle).is_some(),
304 "expected text '{}' to be visible, but it was not found in the display list",
305 needle
306 );
307 }
308
309 pub fn assert_text_not_visible(&self, needle: &str) {
310 assert!(
311 self.find_text(needle).is_none(),
312 "expected text '{}' to NOT be visible, but it was found",
313 needle
314 );
315 }
316}