Skip to content

Commit 5f7511d

Browse files
wthollidayclaude
andcommitted
Add 49 headless unit tests and fix text_editor backspace/delete bug
Tests cover text_editor (cursor, selection, keys, multiline), tap (touch events), list (all orientations), shapes (layout, hittest), padding, size, offset, cond, emptyview, and spacer. Fix: backspace/delete with active selection was double-deleting because delete_selection() cleared the selection state before the check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a7f62f3 commit 5f7511d

10 files changed

Lines changed: 850 additions & 2 deletions

File tree

src/views/cond.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,50 @@ pub fn cond(cond: bool, if_true: impl View, if_false: impl View) -> impl View {
126126
if_false,
127127
}
128128
}
129+
130+
#[cfg(test)]
131+
mod tests {
132+
use super::*;
133+
134+
#[test]
135+
fn test_cond_true_uses_first_view() {
136+
let mut cx = Context::new();
137+
let ui = cond(
138+
true,
139+
rectangle().size([50.0, 50.0]),
140+
rectangle().size([100.0, 100.0]),
141+
);
142+
let sz = [200.0, 200.0].into();
143+
let mut path = vec![0];
144+
let result = ui.layout(
145+
&mut path,
146+
&mut LayoutArgs {
147+
sz,
148+
cx: &mut cx,
149+
text_bounds: &mut |_, _, _| LocalRect::zero(),
150+
},
151+
);
152+
assert_eq!(result, [50.0, 50.0].into());
153+
}
154+
155+
#[test]
156+
fn test_cond_false_uses_second_view() {
157+
let mut cx = Context::new();
158+
let ui = cond(
159+
false,
160+
rectangle().size([50.0, 50.0]),
161+
rectangle().size([100.0, 100.0]),
162+
);
163+
let sz = [200.0, 200.0].into();
164+
let mut path = vec![0];
165+
let result = ui.layout(
166+
&mut path,
167+
&mut LayoutArgs {
168+
sz,
169+
cx: &mut cx,
170+
text_bounds: &mut |_, _, _| LocalRect::zero(),
171+
},
172+
);
173+
assert_eq!(result, [100.0, 100.0].into());
174+
}
175+
}

src/views/emptyview.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,25 @@ impl DynView for EmptyView {
1111
}
1212

1313
impl private::Sealed for EmptyView {}
14+
15+
#[cfg(test)]
16+
mod tests {
17+
use super::*;
18+
19+
#[test]
20+
fn test_emptyview_zero_size() {
21+
let mut cx = Context::new();
22+
let ui = EmptyView {};
23+
let sz = [100.0, 100.0].into();
24+
let mut path = vec![0];
25+
let result = ui.layout(
26+
&mut path,
27+
&mut LayoutArgs {
28+
sz,
29+
cx: &mut cx,
30+
text_bounds: &mut |_, _, _| LocalRect::zero(),
31+
},
32+
);
33+
assert_eq!(result, [0.0, 0.0].into());
34+
}
35+
}

src/views/list.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,104 @@ pub fn zlist<ID: Hash + Clone, V: View, F: Fn(&ID) -> V + Clone + 'static>(
272272
func: f,
273273
}
274274
}
275+
276+
#[cfg(test)]
277+
mod tests {
278+
use super::*;
279+
280+
#[test]
281+
fn test_vertical_list_layout() {
282+
let mut cx = Context::new();
283+
let ui = list(vec![0, 1, 2], |_id| rectangle().size([40.0, 20.0]));
284+
let sz = [200.0, 200.0].into();
285+
let mut path = vec![0];
286+
let result = ui.layout(
287+
&mut path,
288+
&mut LayoutArgs {
289+
sz,
290+
cx: &mut cx,
291+
text_bounds: &mut |_, _, _| LocalRect::zero(),
292+
},
293+
);
294+
// 3 items each 20px tall, max width 40
295+
assert_eq!(result.width, 40.0);
296+
assert_eq!(result.height, 60.0);
297+
}
298+
299+
#[test]
300+
fn test_horizontal_list_layout() {
301+
let mut cx = Context::new();
302+
let ui = hlist(vec![0, 1, 2], |_id| rectangle().size([30.0, 50.0]));
303+
let sz = [200.0, 200.0].into();
304+
let mut path = vec![0];
305+
let result = ui.layout(
306+
&mut path,
307+
&mut LayoutArgs {
308+
sz,
309+
cx: &mut cx,
310+
text_bounds: &mut |_, _, _| LocalRect::zero(),
311+
},
312+
);
313+
// 3 items each 30px wide, max height 50
314+
assert_eq!(result.width, 90.0);
315+
assert_eq!(result.height, 50.0);
316+
}
317+
318+
#[test]
319+
fn test_z_list_layout() {
320+
let mut cx = Context::new();
321+
let ui = zlist(vec![0, 1], |_id| rectangle());
322+
let sz = [100.0, 80.0].into();
323+
let mut path = vec![0];
324+
let result = ui.layout(
325+
&mut path,
326+
&mut LayoutArgs {
327+
sz,
328+
cx: &mut cx,
329+
text_bounds: &mut |_, _, _| LocalRect::zero(),
330+
},
331+
);
332+
// Z layout returns the proposed size
333+
assert_eq!(result, sz);
334+
}
335+
336+
#[test]
337+
fn test_list_empty() {
338+
let mut cx = Context::new();
339+
let ui = list(Vec::<i32>::new(), |_id| rectangle());
340+
let sz = [100.0, 100.0].into();
341+
let mut path = vec![0];
342+
let result = ui.layout(
343+
&mut path,
344+
&mut LayoutArgs {
345+
sz,
346+
cx: &mut cx,
347+
text_bounds: &mut |_, _, _| LocalRect::zero(),
348+
},
349+
);
350+
assert_eq!(result.width, 0.0);
351+
assert_eq!(result.height, 0.0);
352+
}
353+
354+
#[test]
355+
fn test_vertical_list_varying_heights() {
356+
let mut cx = Context::new();
357+
let ui = list(vec![0, 1, 2], |id| {
358+
let h = (*id + 1) as f32 * 10.0;
359+
rectangle().size([50.0, h])
360+
});
361+
let sz = [200.0, 200.0].into();
362+
let mut path = vec![0];
363+
let result = ui.layout(
364+
&mut path,
365+
&mut LayoutArgs {
366+
sz,
367+
cx: &mut cx,
368+
text_bounds: &mut |_, _, _| LocalRect::zero(),
369+
},
370+
);
371+
// Heights: 10 + 20 + 30 = 60
372+
assert_eq!(result.height, 60.0);
373+
assert_eq!(result.width, 50.0);
374+
}
375+
}

src/views/offset.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,47 @@ where
8989
}
9090

9191
impl<V> private::Sealed for Offset<V> {}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use super::*;
96+
97+
#[test]
98+
fn test_offset_preserves_size() {
99+
let mut cx = Context::new();
100+
let ui = Offset::new(rectangle(), [10.0, 20.0].into());
101+
let sz = [100.0, 100.0].into();
102+
let mut path = vec![0];
103+
let result = ui.layout(
104+
&mut path,
105+
&mut LayoutArgs {
106+
sz,
107+
cx: &mut cx,
108+
text_bounds: &mut |_, _, _| LocalRect::zero(),
109+
},
110+
);
111+
// Offset doesn't change the layout size
112+
assert_eq!(result, sz);
113+
}
114+
115+
#[test]
116+
fn test_offset_shifts_hittest() {
117+
let mut cx = Context::new();
118+
let ui = Offset::new(rectangle(), [50.0, 50.0].into());
119+
let sz = [100.0, 100.0].into();
120+
let mut path = vec![0];
121+
ui.layout(
122+
&mut path,
123+
&mut LayoutArgs {
124+
sz,
125+
cx: &mut cx,
126+
text_bounds: &mut |_, _, _| LocalRect::zero(),
127+
},
128+
);
129+
// The rectangle is offset by (50,50), so point (25,25) maps to (-25,-25) in child space
130+
// which is outside the 100x100 rect
131+
assert!(ui.hittest(&mut path, [25.0, 25.0].into(), &mut cx).is_none());
132+
// Point (75,75) maps to (25,25) in child space, which is inside
133+
assert!(ui.hittest(&mut path, [75.0, 75.0].into(), &mut cx).is_some());
134+
}
135+
}

src/views/padding.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,64 @@ where
120120
}
121121

122122
impl<V> private::Sealed for Padding<V> {}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use super::*;
127+
128+
#[test]
129+
fn test_padding_increases_size() {
130+
let mut cx = Context::new();
131+
let ui = Padding::new(rectangle(), PaddingParam::Px(10.0));
132+
let sz = [100.0, 100.0].into();
133+
let mut path = vec![0];
134+
let result = ui.layout(
135+
&mut path,
136+
&mut LayoutArgs {
137+
sz,
138+
cx: &mut cx,
139+
text_bounds: &mut |_, _, _| LocalRect::zero(),
140+
},
141+
);
142+
// rectangle fills available space (100-20=80), then padding adds 20 back
143+
assert_eq!(result, [100.0, 100.0].into());
144+
}
145+
146+
#[test]
147+
fn test_padding_auto() {
148+
let mut cx = Context::new();
149+
let ui = Padding::new(rectangle(), PaddingParam::Auto);
150+
let sz = [50.0, 50.0].into();
151+
let mut path = vec![0];
152+
let result = ui.layout(
153+
&mut path,
154+
&mut LayoutArgs {
155+
sz,
156+
cx: &mut cx,
157+
text_bounds: &mut |_, _, _| LocalRect::zero(),
158+
},
159+
);
160+
// Auto padding is 5.0, so child gets 40x40, result is 50x50
161+
assert_eq!(result, [50.0, 50.0].into());
162+
}
163+
164+
#[test]
165+
fn test_padding_hittest_offsets() {
166+
let mut cx = Context::new();
167+
let ui = Padding::new(rectangle(), PaddingParam::Px(20.0));
168+
let sz = [100.0, 100.0].into();
169+
let mut path = vec![0];
170+
ui.layout(
171+
&mut path,
172+
&mut LayoutArgs {
173+
sz,
174+
cx: &mut cx,
175+
text_bounds: &mut |_, _, _| LocalRect::zero(),
176+
},
177+
);
178+
// Point inside the padded child area (child starts at 20,20)
179+
assert!(ui.hittest(&mut path, [50.0, 50.0].into(), &mut cx).is_some());
180+
// Point in the padding area (outside child)
181+
assert!(ui.hittest(&mut path, [5.0, 5.0].into(), &mut cx).is_none());
182+
}
183+
}

0 commit comments

Comments
 (0)