use nalgebra_glm::{Vec2, Vec4};
use winit::keyboard::KeyCode;
use crate::ecs::input::resources::MouseState;
use crate::ecs::ui::resources::UiEvent;
use crate::ecs::world::World;
use super::driver::UiTestDriver;
use super::types::{
ActionRecord, AssertKind, EventMatch, ReportSummary, ResultRecord, ScheduledAction, TestAction,
TestReport, UiTestResult,
};
use crate::prelude::*;
impl UiTestDriver {
fn clear_frame_input(&self, world: &mut World) {
world.resources.input.mouse.state = MouseState::empty();
world.resources.input.mouse.position_delta = Vec2::zeros();
world.resources.input.mouse.wheel_delta = Vec2::zeros();
world.resources.input.keyboard.just_pressed_keys.clear();
world.resources.input.keyboard.just_released_keys.clear();
world.resources.input.keyboard.frame_keys.clear();
world.resources.input.keyboard.frame_chars.clear();
}
pub fn step(&mut self, world: &mut World) {
if !self.active {
return;
}
self.captured_events
.extend(world.resources.retained_ui.frame.events.iter().cloned());
self.clear_frame_input(world);
if self.pending_release {
self.pending_release = false;
world
.resources
.input
.mouse
.state
.insert(MouseState::LEFT_JUST_RELEASED);
}
if self.pending_right_release {
self.pending_right_release = false;
world
.resources
.input
.mouse
.state
.insert(MouseState::RIGHT_JUST_RELEASED);
}
if !self.pending_key_releases.is_empty() {
let keys = std::mem::take(&mut self.pending_key_releases);
for key in &keys {
world
.resources
.input
.keyboard
.keystates
.insert(*key, winit::event::ElementState::Released);
world
.resources
.input
.keyboard
.just_released_keys
.insert(*key);
world
.resources
.input
.keyboard
.frame_keys
.push((*key, false));
}
}
self.process_frame_actions(world);
self.frame += 1;
if self.cursor >= self.actions.len()
&& !self.pending_release
&& !self.pending_right_release
&& self.pending_key_releases.is_empty()
{
self.active = false;
self.print_results();
if self.exit_on_complete {
let code = if self.all_passed() { 0 } else { 1 };
std::process::exit(code);
}
}
}
fn process_frame_actions(&mut self, world: &mut World) {
while self.cursor < self.actions.len() && self.actions[self.cursor].frame == self.frame {
let action = self.actions[self.cursor].action.clone();
self.execute_action(world, action);
self.cursor += 1;
}
}
pub(super) fn resolve_entity_center(&self, world: &World, test_id: &str) -> Option<Vec2> {
let entity = *world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id)?;
let node = world.ui.get_ui_layout_node(entity)?;
Some(node.computed_rect.center())
}
fn resolve_raw_entity_center(&self, world: &World, entity: freecs::Entity) -> Option<Vec2> {
let node = world.ui.get_ui_layout_node(entity)?;
Some(node.computed_rect.center())
}
fn inject_click_at(&mut self, world: &mut World, position: Vec2) {
let delta = position - world.resources.input.mouse.position;
world.resources.input.mouse.position = position;
world.resources.input.mouse.position_delta = delta;
world
.resources
.input
.mouse
.state
.insert(MouseState::MOVED | MouseState::LEFT_JUST_PRESSED | MouseState::LEFT_CLICKED);
self.pending_release = true;
}
fn build_parent_map(
world: &World,
) -> std::collections::HashMap<freecs::Entity, freecs::Entity> {
let mut map = std::collections::HashMap::new();
for (parent, children) in world.resources.transform_state.children_cache.iter() {
for &child in children {
map.insert(child, *parent);
}
}
map
}
fn scroll_into_view(&mut self, world: &mut World, test_id: &str) -> bool {
let entity = match world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id)
.copied()
{
Some(e) => e,
None => return false,
};
let target_center = match world.ui.get_ui_layout_node(entity) {
Some(node) => node.computed_rect.center(),
None => return false,
};
let parents = Self::build_parent_map(world);
let mut current = entity;
let mut outermost_scroll: Option<freecs::Entity> = None;
while let Some(&parent) = parents.get(¤t) {
if world.ui.get_ui_scroll_area(parent).is_some() {
outermost_scroll = Some(parent);
}
current = parent;
}
let scroll_entity = match outermost_scroll {
Some(e) => e,
None => return false,
};
let scroll_rect = match world.ui.get_ui_layout_node(scroll_entity) {
Some(node) => node.computed_rect,
None => return false,
};
if scroll_rect.contains(target_center) {
return false;
}
let dpi_scale = world.resources.window.cached_scale_factor.max(1.0);
let (current_offset, content_entity) = match world.ui.get_ui_scroll_area(scroll_entity) {
Some(data) => (data.scroll_offset, data.content_entity),
None => return false,
};
let scroll_center_y = scroll_rect.center().y;
let delta_pixels = target_center.y - scroll_center_y;
let delta_units = delta_pixels / dpi_scale;
let new_offset = (current_offset + delta_units).max(0.0);
if (new_offset - current_offset).abs() < 0.5 {
return false;
}
if let Some(data_mut) = world.ui.get_ui_scroll_area_mut(scroll_entity) {
data_mut.scroll_offset = new_offset;
}
if let Some(content_node) = world.ui.get_ui_layout_node_mut(content_entity) {
content_node.scroll_offset = Vec2::new(0.0, new_offset);
}
true
}
fn execute_action(&mut self, world: &mut World, action: TestAction) {
self.record_action(&action, world);
match action {
TestAction::MoveMouse(position) => {
let delta = position - world.resources.input.mouse.position;
world.resources.input.mouse.position = position;
world.resources.input.mouse.position_delta = delta;
world.resources.input.mouse.state.insert(MouseState::MOVED);
}
TestAction::MoveMouseToEntity(test_id) => {
if let Some(center) = self.resolve_entity_center(world, &test_id) {
let delta = center - world.resources.input.mouse.position;
world.resources.input.mouse.position = center;
world.resources.input.mouse.position_delta = delta;
world.resources.input.mouse.state.insert(MouseState::MOVED);
} else {
eprintln!("[UI_TEST] WARNING: entity '{test_id}' not found for move_mouse_to");
}
}
TestAction::MousePress => {
world
.resources
.input
.mouse
.state
.insert(MouseState::LEFT_JUST_PRESSED | MouseState::LEFT_CLICKED);
}
TestAction::MouseRelease => {
world
.resources
.input
.mouse
.state
.insert(MouseState::LEFT_JUST_RELEASED);
}
TestAction::RightMousePress => {
world
.resources
.input
.mouse
.state
.insert(MouseState::RIGHT_JUST_PRESSED | MouseState::RIGHT_CLICKED);
}
TestAction::RightMouseRelease => {
world
.resources
.input
.mouse
.state
.insert(MouseState::RIGHT_JUST_RELEASED);
}
TestAction::ClickAt(position) => {
self.inject_click_at(world, position);
}
TestAction::ClickAtEntity(test_id) => {
if self.scroll_into_view(world, &test_id) {
self.actions.push(ScheduledAction {
frame: self.frame + 1,
action: TestAction::ClickAtEntity(test_id),
});
self.actions.sort_by_key(|a| a.frame);
} else if let Some(center) = self.resolve_entity_center(world, &test_id) {
self.inject_click_at(world, center);
} else {
eprintln!("[UI_TEST] WARNING: entity '{test_id}' not found for click_entity");
}
}
TestAction::RightClickAt(position) => {
let delta = position - world.resources.input.mouse.position;
world.resources.input.mouse.position = position;
world.resources.input.mouse.position_delta = delta;
world.resources.input.mouse.state.insert(
MouseState::MOVED | MouseState::RIGHT_JUST_PRESSED | MouseState::RIGHT_CLICKED,
);
self.pending_right_release = true;
}
TestAction::RightClickAtEntity(test_id) => {
if let Some(center) = self.resolve_entity_center(world, &test_id) {
let delta = center - world.resources.input.mouse.position;
world.resources.input.mouse.position = center;
world.resources.input.mouse.position_delta = delta;
world.resources.input.mouse.state.insert(
MouseState::MOVED
| MouseState::RIGHT_JUST_PRESSED
| MouseState::RIGHT_CLICKED,
);
self.pending_right_release = true;
} else {
eprintln!(
"[UI_TEST] WARNING: entity '{test_id}' not found for right_click_entity"
);
}
}
TestAction::DragEntity(test_id, end_position) => {
if let Some(center) = self.resolve_entity_center(world, &test_id) {
let delta = center - world.resources.input.mouse.position;
world.resources.input.mouse.position = center;
world.resources.input.mouse.position_delta = delta;
world.resources.input.mouse.state.insert(
MouseState::MOVED
| MouseState::LEFT_JUST_PRESSED
| MouseState::LEFT_CLICKED,
);
let release_frame = self.frame + 1;
self.actions.push(ScheduledAction {
frame: release_frame,
action: TestAction::MoveMouse(end_position),
});
self.actions.push(ScheduledAction {
frame: release_frame,
action: TestAction::MouseRelease,
});
self.actions.sort_by_key(|a| a.frame);
} else {
eprintln!("[UI_TEST] WARNING: entity '{test_id}' not found for drag_entity");
}
}
TestAction::SetSliderValue(test_id, target_value) => {
if self.scroll_into_view(world, &test_id) {
self.actions.push(ScheduledAction {
frame: self.frame + 1,
action: TestAction::SetSliderValue(test_id, target_value),
});
self.actions.sort_by_key(|a| a.frame);
return;
}
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(&test_id)
.copied();
if let Some(entity) = entity {
let slider_info = if let Some(data) = world.ui.get_ui_slider(entity) {
let rect = world
.ui
.get_ui_layout_node(entity)
.map(|node| node.computed_rect);
rect.map(|rect| {
let range = data.max - data.min;
let normalized = if data.logarithmic && data.min > 0.0 && range > 0.0 {
((target_value / data.min).ln() / (data.max / data.min).ln())
.clamp(0.0, 1.0)
} else if range.abs() > f32::EPSILON {
((target_value - data.min) / range).clamp(0.0, 1.0)
} else {
0.0
};
let target_x = rect.min.x + normalized * rect.width();
let target_y = rect.center().y;
Vec2::new(target_x, target_y)
})
} else {
None
};
if let Some(position) = slider_info {
self.inject_click_at(world, position);
} else {
eprintln!(
"[UI_TEST] WARNING: '{test_id}' is not a slider or has no layout"
);
}
} else {
eprintln!(
"[UI_TEST] WARNING: entity '{test_id}' not found for set_slider_value"
);
}
}
TestAction::SelectDropdownOption(test_id, option_index) => {
if self.scroll_into_view(world, &test_id) {
self.actions.push(ScheduledAction {
frame: self.frame + 1,
action: TestAction::SelectDropdownOption(test_id, option_index),
});
self.actions.sort_by_key(|a| a.frame);
return;
}
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(&test_id)
.copied();
if let Some(entity) = entity {
let option_count = if let Some(data) = world.ui.get_ui_dropdown(entity) {
data.options.len()
} else {
0
};
if option_count > 0 {
if let Some(center) = self.resolve_entity_center(world, &test_id) {
self.inject_click_at(world, center);
}
let nav_start = self.frame + 2;
let arrow_count = if option_index == 0 {
option_count
} else {
option_index
};
for step in 0..arrow_count {
self.actions.push(ScheduledAction {
frame: nav_start + step as u64,
action: TestAction::PressAndReleaseKey(KeyCode::ArrowDown),
});
}
self.actions.push(ScheduledAction {
frame: nav_start + arrow_count as u64,
action: TestAction::PressAndReleaseKey(KeyCode::Enter),
});
self.actions.sort_by_key(|a| a.frame);
} else {
eprintln!(
"[UI_TEST] WARNING: '{test_id}' is not a dropdown or has no options"
);
}
} else {
eprintln!(
"[UI_TEST] WARNING: entity '{test_id}' not found for select_dropdown_option"
);
}
}
TestAction::SelectTab(test_id, tab_index) => {
if self.scroll_into_view(world, &test_id) {
self.actions.push(ScheduledAction {
frame: self.frame + 1,
action: TestAction::SelectTab(test_id, tab_index),
});
self.actions.sort_by_key(|a| a.frame);
return;
}
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(&test_id)
.copied();
if let Some(entity) = entity {
let tab_entity = if let Some(data) = world.ui.get_ui_tab_bar(entity) {
data.tab_entities.get(tab_index).copied()
} else {
None
};
if let Some(tab_entity) = tab_entity {
if let Some(center) = self.resolve_raw_entity_center(world, tab_entity) {
self.inject_click_at(world, center);
} else {
eprintln!(
"[UI_TEST] WARNING: tab {tab_index} in '{test_id}' has no layout"
);
}
} else {
eprintln!(
"[UI_TEST] WARNING: '{test_id}' is not a tab bar or index {tab_index} out of range"
);
}
} else {
eprintln!("[UI_TEST] WARNING: entity '{test_id}' not found for select_tab");
}
}
TestAction::TypeChars(chars) => {
for character in &chars {
world.resources.input.keyboard.frame_chars.push(*character);
}
}
TestAction::PressAndReleaseKey(key) => {
world
.resources
.input
.keyboard
.keystates
.insert(key, winit::event::ElementState::Pressed);
world.resources.input.keyboard.just_pressed_keys.insert(key);
world.resources.input.keyboard.frame_keys.push((key, true));
self.pending_key_releases.push(key);
}
TestAction::KeyDown(key) => {
world
.resources
.input
.keyboard
.keystates
.insert(key, winit::event::ElementState::Pressed);
world.resources.input.keyboard.just_pressed_keys.insert(key);
world.resources.input.keyboard.frame_keys.push((key, true));
}
TestAction::KeyUp(key) => {
world
.resources
.input
.keyboard
.keystates
.insert(key, winit::event::ElementState::Released);
world
.resources
.input
.keyboard
.just_released_keys
.insert(key);
world.resources.input.keyboard.frame_keys.push((key, false));
}
TestAction::Scroll(delta) => {
world.resources.input.mouse.wheel_delta = delta;
world
.resources
.input
.mouse
.state
.insert(MouseState::SCROLLED);
}
TestAction::Screenshot(_path) => {
#[cfg(not(target_arch = "wasm32"))]
{
crate::ecs::world::commands::capture_screenshot_to_path(world, &_path);
self.screenshots.push(_path);
}
}
TestAction::Assert(name, kind) => {
let result = self.evaluate_assertion(world, &name, &kind);
self.result_frames.push(self.frame);
self.results.push(result);
}
TestAction::Log(message) => {
eprintln!("[UI_TEST] {message}");
}
TestAction::Exit(success) => {
self.active = false;
self.print_results();
let code = if success { 0 } else { 1 };
std::process::exit(code);
}
}
}
fn evaluate_assertion(&self, world: &World, name: &str, kind: &AssertKind) -> UiTestResult {
match kind {
AssertKind::Visible(test_id) => {
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id);
let visible = entity
.and_then(|entity| world.ui.get_ui_layout_node(*entity))
.is_some_and(|node| node.visible);
UiTestResult {
name: name.to_string(),
passed: visible,
message: if visible {
format!("'{test_id}' is visible")
} else {
format!("'{test_id}' expected visible but was not")
},
}
}
AssertKind::NotVisible(test_id) => {
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id);
let visible = entity
.and_then(|entity| world.ui.get_ui_layout_node(*entity))
.is_some_and(|node| node.visible);
UiTestResult {
name: name.to_string(),
passed: !visible,
message: if !visible {
format!("'{test_id}' is not visible")
} else {
format!("'{test_id}' expected not visible but was visible")
},
}
}
AssertKind::Focused(test_id) => {
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id);
let focused = entity.is_some_and(|entity| {
world
.resources
.retained_ui
.interaction_for_active()
.focused_entity
== Some(*entity)
});
UiTestResult {
name: name.to_string(),
passed: focused,
message: if focused {
format!("'{test_id}' is focused")
} else {
format!("'{test_id}' expected focused but was not")
},
}
}
AssertKind::NotFocused(test_id) => {
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id);
let focused = entity.is_some_and(|entity| {
world
.resources
.retained_ui
.interaction_for_active()
.focused_entity
== Some(*entity)
});
UiTestResult {
name: name.to_string(),
passed: !focused,
message: if !focused {
format!("'{test_id}' is not focused")
} else {
format!("'{test_id}' expected not focused but was focused")
},
}
}
AssertKind::Hovered(test_id) => {
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id);
let hovered = entity
.and_then(|entity| world.ui.get_ui_node_interaction(*entity))
.is_some_and(|interaction| interaction.hovered);
UiTestResult {
name: name.to_string(),
passed: hovered,
message: if hovered {
format!("'{test_id}' is hovered")
} else {
format!("'{test_id}' expected hovered but was not")
},
}
}
AssertKind::NotHovered(test_id) => {
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id);
let hovered = entity
.and_then(|entity| world.ui.get_ui_node_interaction(*entity))
.is_some_and(|interaction| interaction.hovered);
UiTestResult {
name: name.to_string(),
passed: !hovered,
message: if !hovered {
format!("'{test_id}' is not hovered")
} else {
format!("'{test_id}' expected not hovered but was hovered")
},
}
}
AssertKind::Disabled(test_id) => {
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id);
let disabled = entity
.and_then(|entity| world.ui.get_ui_node_interaction(*entity))
.is_some_and(|interaction| interaction.disabled);
UiTestResult {
name: name.to_string(),
passed: disabled,
message: if disabled {
format!("'{test_id}' is disabled")
} else {
format!("'{test_id}' expected disabled but was not")
},
}
}
AssertKind::NotDisabled(test_id) => {
let entity = world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id);
let disabled = entity
.and_then(|entity| world.ui.get_ui_node_interaction(*entity))
.is_some_and(|interaction| interaction.disabled);
UiTestResult {
name: name.to_string(),
passed: !disabled,
message: if !disabled {
format!("'{test_id}' is enabled")
} else {
format!("'{test_id}' expected enabled but was disabled")
},
}
}
AssertKind::TextEquals(test_id, expected) => {
let actual = self.get_text_content(world, test_id);
let passed = actual.as_deref() == Some(expected.as_str());
UiTestResult {
name: name.to_string(),
passed,
message: if passed {
format!("'{test_id}' text equals '{expected}'")
} else {
format!(
"'{test_id}' expected text '{expected}', got '{}'",
actual.as_deref().unwrap_or("<none>")
)
},
}
}
AssertKind::WidgetValueF32(test_id, expected, tolerance) => {
match read_widget_f32(world, test_id) {
Some(actual) => {
let passed = (actual - expected).abs() <= *tolerance;
UiTestResult {
name: name.to_string(),
passed,
message: if passed {
format!("'{test_id}' value {actual} matches expected {expected}")
} else {
format!(
"'{test_id}' expected {expected} (tolerance {tolerance}), got {actual}"
)
},
}
}
None => UiTestResult {
name: name.to_string(),
passed: false,
message: format!("'{test_id}' has no f32 widget value"),
},
}
}
AssertKind::WidgetValueBool(test_id, expected) => {
match read_widget_bool(world, test_id) {
Some(actual) => {
let passed = actual == *expected;
UiTestResult {
name: name.to_string(),
passed,
message: if passed {
format!("'{test_id}' is {expected}")
} else {
format!("'{test_id}' expected {expected}, got {actual}")
},
}
}
None => UiTestResult {
name: name.to_string(),
passed: false,
message: format!("'{test_id}' has no bool widget value"),
},
}
}
AssertKind::WidgetValueUsize(test_id, expected) => {
match read_widget_usize(world, test_id) {
Some(actual) => {
let passed = actual == *expected;
UiTestResult {
name: name.to_string(),
passed,
message: if passed {
format!("'{test_id}' is {expected}")
} else {
format!("'{test_id}' expected {expected}, got {actual}")
},
}
}
None => UiTestResult {
name: name.to_string(),
passed: false,
message: format!("'{test_id}' has no usize widget value"),
},
}
}
AssertKind::WidgetValueString(test_id, expected) => {
match read_widget_string(world, test_id) {
Some(actual) => {
let passed = actual == *expected;
UiTestResult {
name: name.to_string(),
passed,
message: if passed {
format!("'{test_id}' equals '{expected}'")
} else {
format!("'{test_id}' expected '{expected}', got '{actual}'")
},
}
}
None => UiTestResult {
name: name.to_string(),
passed: false,
message: format!("'{test_id}' has no string widget value"),
},
}
}
AssertKind::WidgetValueVec4(test_id, expected, tolerance) => {
match read_widget_vec4(world, test_id) {
Some(actual) => {
let diff = (actual - expected).norm();
let passed = diff <= *tolerance;
UiTestResult {
name: name.to_string(),
passed,
message: if passed {
format!("'{test_id}' matches expected value")
} else {
format!(
"'{test_id}' expected [{:.3}, {:.3}, {:.3}, {:.3}], got [{:.3}, {:.3}, {:.3}, {:.3}]",
expected.x,
expected.y,
expected.z,
expected.w,
actual.x,
actual.y,
actual.z,
actual.w,
)
},
}
}
None => UiTestResult {
name: name.to_string(),
passed: false,
message: format!("'{test_id}' has no Vec4 widget value"),
},
}
}
AssertKind::EventFired(event_match) => {
let found = self.captured_events.iter().any(|event| match event_match {
EventMatch::ButtonClicked => matches!(event, UiEvent::ButtonClicked(_)),
EventMatch::SliderChanged => matches!(event, UiEvent::SliderChanged { .. }),
EventMatch::ToggleChanged => matches!(event, UiEvent::ToggleChanged { .. }),
EventMatch::TextInputSubmitted => {
matches!(event, UiEvent::TextInputSubmitted { .. })
}
EventMatch::TextInputChanged => {
matches!(event, UiEvent::TextInputChanged { .. })
}
EventMatch::CheckboxChanged => {
matches!(event, UiEvent::CheckboxChanged { .. })
}
EventMatch::DropdownChanged => {
matches!(event, UiEvent::DropdownChanged { .. })
}
EventMatch::RadioChanged => matches!(event, UiEvent::RadioChanged { .. }),
EventMatch::TabChanged => matches!(event, UiEvent::TabChanged { .. }),
EventMatch::DragValueChanged => {
matches!(event, UiEvent::DragValueChanged { .. })
}
EventMatch::MenuItemClicked => {
matches!(event, UiEvent::MenuItemClicked { .. })
}
EventMatch::ColorPickerChanged => {
matches!(event, UiEvent::ColorPickerChanged { .. })
}
EventMatch::SelectableLabelClicked => {
matches!(event, UiEvent::SelectableLabelClicked { .. })
}
EventMatch::ContextMenuItemClicked => {
matches!(event, UiEvent::ContextMenuItemClicked { .. })
}
EventMatch::TreeNodeSelected => {
matches!(event, UiEvent::TreeNodeSelected { .. })
}
EventMatch::TreeNodeToggled => {
matches!(event, UiEvent::TreeNodeToggled { .. })
}
EventMatch::ModalClosed => matches!(event, UiEvent::ModalClosed { .. }),
EventMatch::CommandPaletteExecuted => {
matches!(event, UiEvent::CommandPaletteExecuted { .. })
}
EventMatch::TileTabActivated => {
matches!(event, UiEvent::TileTabActivated { .. })
}
EventMatch::TileTabClosed => matches!(event, UiEvent::TileTabClosed { .. }),
EventMatch::TileSplitterMoved => {
matches!(event, UiEvent::TileSplitterMoved { .. })
}
EventMatch::CanvasClicked => matches!(event, UiEvent::CanvasClicked { .. }),
EventMatch::VirtualListItemClicked => {
matches!(event, UiEvent::VirtualListItemClicked { .. })
}
EventMatch::TextAreaChanged => {
matches!(event, UiEvent::TextAreaChanged { .. })
}
EventMatch::RichTextEditorChanged => {
matches!(event, UiEvent::RichTextEditorChanged { .. })
}
EventMatch::DataGridFilterChanged => {
matches!(event, UiEvent::DataGridFilterChanged { .. })
}
EventMatch::RangeSliderChanged => {
matches!(event, UiEvent::RangeSliderChanged { .. })
}
EventMatch::DataGridCellEdited => {
matches!(event, UiEvent::DataGridCellEdited { .. })
}
EventMatch::BreadcrumbClicked => {
matches!(event, UiEvent::BreadcrumbClicked { .. })
}
EventMatch::SplitterChanged => {
matches!(event, UiEvent::SplitterChanged { .. })
}
EventMatch::MultiSelectChanged => {
matches!(event, UiEvent::MultiSelectChanged { .. })
}
EventMatch::DatePickerChanged => {
matches!(event, UiEvent::DatePickerChanged { .. })
}
EventMatch::DragStarted => matches!(event, UiEvent::DragStarted { .. }),
EventMatch::DragDropped => matches!(event, UiEvent::DragDropped { .. }),
EventMatch::DragCancelled => matches!(event, UiEvent::DragCancelled { .. }),
});
UiTestResult {
name: name.to_string(),
passed: found,
message: if found {
format!("Event {event_match:?} was fired")
} else {
format!("Event {event_match:?} expected but not fired")
},
}
}
}
}
fn get_text_content(&self, world: &World, test_id: &str) -> Option<String> {
let entity = *world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id)?;
let text_slot = ui_text_slot_for_entity(world, entity)?;
world
.resources
.text
.cache
.get_text(text_slot)
.map(|s| s.to_string())
}
fn record_action(&mut self, action: &TestAction, world: &World) {
let (kind, target, position) = match action {
TestAction::MoveMouse(p) => ("move_mouse", None, Some(*p)),
TestAction::MoveMouseToEntity(id) => (
"move_mouse_to_entity",
Some(id.clone()),
self.resolve_entity_center(world, id),
),
TestAction::MousePress => ("mouse_press", None, None),
TestAction::MouseRelease => ("mouse_release", None, None),
TestAction::RightMousePress => ("right_mouse_press", None, None),
TestAction::RightMouseRelease => ("right_mouse_release", None, None),
TestAction::ClickAt(p) => ("click_at", None, Some(*p)),
TestAction::ClickAtEntity(id) => (
"click_at_entity",
Some(id.clone()),
self.resolve_entity_center(world, id),
),
TestAction::RightClickAt(p) => ("right_click_at", None, Some(*p)),
TestAction::RightClickAtEntity(id) => (
"right_click_at_entity",
Some(id.clone()),
self.resolve_entity_center(world, id),
),
TestAction::DragEntity(id, _) => ("drag_entity", Some(id.clone()), None),
TestAction::SetSliderValue(id, _) => ("set_slider_value", Some(id.clone()), None),
TestAction::SelectDropdownOption(id, _) => {
("select_dropdown_option", Some(id.clone()), None)
}
TestAction::SelectTab(id, _) => ("select_tab", Some(id.clone()), None),
TestAction::TypeChars(_) => ("type_chars", None, None),
TestAction::PressAndReleaseKey(_) => ("press_and_release_key", None, None),
TestAction::KeyDown(_) => ("key_down", None, None),
TestAction::KeyUp(_) => ("key_up", None, None),
TestAction::Scroll(p) => ("scroll", None, Some(*p)),
TestAction::Screenshot(path) => ("screenshot", Some(path.clone()), None),
TestAction::Assert(name, _) => ("assert", Some(name.clone()), None),
TestAction::Log(msg) => ("log", Some(msg.clone()), None),
TestAction::Exit(_) => ("exit", None, None),
};
self.action_records.push(ActionRecord {
frame: self.frame,
kind: kind.to_string(),
target,
position: position.map(|p| [p.x, p.y]),
});
}
pub(super) fn write_json_report(&self) {
let Some(path) = &self.json_output_path else {
return;
};
let report = TestReport {
test_name: self.test_name.clone(),
passed: self.all_passed(),
summary: ReportSummary {
total: self.results.len(),
passed: self.results.iter().filter(|r| r.passed).count(),
failed: self.results.iter().filter(|r| !r.passed).count(),
},
results: self
.results
.iter()
.enumerate()
.map(|(index, result)| ResultRecord {
name: result.name.clone(),
passed: result.passed,
message: result.message.clone(),
frame: self.result_frames.get(index).copied().unwrap_or(0),
})
.collect(),
actions: self.action_records.clone(),
screenshots: self.screenshots.clone(),
};
match serde_json::to_string_pretty(&report) {
Ok(json) => {
if let Err(error) = std::fs::write(path, &json) {
eprintln!("[UI_TEST] failed to write JSON report to {path}: {error}");
}
}
Err(error) => {
eprintln!("[UI_TEST] failed to serialize JSON report: {error}");
}
}
}
pub(super) fn print_results(&self) {
self.write_json_report();
eprintln!();
eprintln!("=== UI Test: {} ===", self.test_name);
let mut pass_count = 0;
let mut fail_count = 0;
for result in &self.results {
if result.passed {
pass_count += 1;
eprintln!(" PASS: {} - {}", result.name, result.message);
} else {
fail_count += 1;
eprintln!(" FAIL: {} - {}", result.name, result.message);
}
}
eprintln!();
eprintln!(
"Results: {} passed, {} failed, {} total",
pass_count,
fail_count,
self.results.len()
);
if fail_count == 0 {
eprintln!("ALL TESTS PASSED");
} else {
eprintln!("TESTS FAILED");
}
eprintln!("========================");
eprintln!();
}
}
fn entity_for_test_id(world: &World, test_id: &str) -> Option<freecs::Entity> {
world
.resources
.retained_ui
.accessibility
.test_id_map
.get(test_id)
.copied()
}
fn read_widget_f32(world: &World, test_id: &str) -> Option<f32> {
let entity = entity_for_test_id(world, test_id)?;
if let Some(data) = world.ui.get_ui_slider(entity) {
return Some(data.value);
}
if let Some(data) = world.ui.get_ui_drag_value(entity) {
return Some(data.value);
}
if let Some(data) = world.ui.get_ui_progress_bar(entity) {
return Some(data.value);
}
None
}
fn read_widget_bool(world: &World, test_id: &str) -> Option<bool> {
let entity = entity_for_test_id(world, test_id)?;
if let Some(data) = world.ui.get_ui_toggle(entity) {
return Some(data.value);
}
if let Some(data) = world.ui.get_ui_checkbox(entity) {
return Some(data.value);
}
if let Some(data) = world.ui.get_ui_selectable_label(entity) {
return Some(data.selected);
}
if let Some(data) = world.ui.get_ui_radio(entity) {
return Some(data.selected);
}
if let Some(data) = world.ui.get_ui_collapsing_header(entity) {
return Some(data.open);
}
None
}
fn read_widget_usize(world: &World, test_id: &str) -> Option<usize> {
let entity = entity_for_test_id(world, test_id)?;
if let Some(data) = world.ui.get_ui_dropdown(entity) {
return Some(data.selected_index);
}
if let Some(data) = world.ui.get_ui_tab_bar(entity) {
return Some(data.selected_tab);
}
None
}
fn read_widget_string(world: &World, test_id: &str) -> Option<String> {
let entity = entity_for_test_id(world, test_id)?;
if let Some(data) = world.ui.get_ui_text_input(entity) {
return Some(data.text.clone());
}
if let Some(data) = world.ui.get_ui_text_area(entity) {
return Some(data.text.clone());
}
if let Some(data) = world.ui.get_ui_rich_text_editor(entity) {
return Some(data.text.clone());
}
None
}
fn read_widget_vec4(world: &World, test_id: &str) -> Option<Vec4> {
let entity = entity_for_test_id(world, test_id)?;
if let Some(data) = world.ui.get_ui_color_picker(entity) {
return Some(data.color);
}
None
}