1use ui_core::application::{ApplicationHost, ComponentRunnerInputs};
13use ui_core::headless::FrameHarness;
14use ui_core::launch::LaunchError;
15use ui_core::permissions::{Role, UserId};
16use ui_core::record::RecordId;
17
18use tilo_resource::Resource;
19
20use super::app::{ResourceApp, definition, prepare_on, role_permissions};
21use super::headless::{AUTHOR, INSTANCE, TEST_WORKSPACE, operation, profile_directory, surface_of};
22use super::root::ResourceRoot;
23
24pub use super::headless::{HeadlessApp, HostedApp, surface_in};
25
26#[cfg(feature = "test-support")]
30pub use ui_core::testing::{hang_bound, hang_deadline, wait_for, wait_until};
31
32#[cfg(feature = "test-support")]
34pub mod perf;
35
36#[cfg(feature = "test-support")]
38mod reach;
39#[cfg(feature = "test-support")]
40pub use reach::{
41 check_agents_are_shown_every_action, check_scenarios_reach_every_action, unreachable_actions,
42 unreachable_actions_in_share,
43};
44
45pub type TestApp<A> = HeadlessApp<A>;
47
48pub const SIDEBAR_BUTTON_WINDOWS: [(u32, u32); 2] = [(1280, 800), (400, 800)];
52
53pub fn check_window_sidebar_button<A: ResourceApp>(
60 mut open: impl FnMut() -> Result<HeadlessApp<A>, LaunchError>,
61) -> Result<(), String> {
62 for viewport in SIDEBAR_BUTTON_WINDOWS {
63 let mut app = open().map_err(|error| format!("{viewport:?}: the app opens: {error}"))?;
64 let harness = app.harness();
65 harness
66 .resize(viewport)
67 .map_err(|error| format!("{viewport:?}: {error}"))?;
68 harness.present_in_window_bar();
69 harness
70 .check_window_sidebar_button()
71 .map_err(|error| format!("{viewport:?}: {error}"))?;
72 }
73 Ok(())
74}
75
76pub fn with_surface<A: ResourceApp, S: 'static, T>(
82 harness: &mut FrameHarness,
83 visit: impl FnOnce(&mut S) -> T,
84) -> Option<T> {
85 harness
86 .with_widget_mut(|root: &mut ResourceRoot| surface_of::<A, S>(root).map(visit))
87 .flatten()
88}
89
90pub fn surface_in_root<A: ResourceApp, S: 'static>(
94 widget: &mut dyn ui_core::widget::Widget,
95) -> Option<&mut S> {
96 surface_of::<A, S>(widget.as_any_mut()?.downcast_mut::<ResourceRoot>()?)
97}
98
99pub fn detached_dispatcher<R: Resource>(author: &str) -> super::Dispatcher<R> {
107 super::Dispatcher::new("test", R::KIND, false, Some(author))
108}
109
110pub fn detached_live<R: Resource>(
114 doc: &tilo_resource::Doc,
115) -> Result<super::Live<R>, tilo_resource::ModelError> {
116 Ok(super::Live::new(tilo_resource::Reader::read(doc)?))
117}
118
119pub fn inspect_live_window<A: ResourceApp, T>(
124 inspect: impl FnOnce(&ui_core::App) -> T,
125) -> Result<T, LaunchError> {
126 drive_live_window::<A, T>(|app| inspect(app))
127}
128
129pub fn drive_live_window<A: ResourceApp, T>(
133 drive: impl FnOnce(&mut ui_core::App) -> T,
134) -> Result<T, LaunchError> {
135 drive_live_window_writing_after::<A, T>(app_state::DEFAULT_DEBOUNCE, drive)
136}
137
138pub fn drive_live_window_writing_after<A: ResourceApp, T>(
145 debounce: ui_core::time::Duration,
146 drive: impl FnOnce(&mut ui_core::App) -> T,
147) -> Result<T, LaunchError> {
148 let profile = profile_directory().map_err(operation)?;
149 let mut host = ApplicationHost::with_store(
150 std::rc::Rc::new(app_state::FsStore::with_debounce(
151 profile.path().join("state"),
152 debounce,
153 )),
154 "test-device".into(),
155 "test-workspace",
156 UserId(AUTHOR.into()),
157 role_permissions(AUTHOR, Role::Owner),
158 tilo_actions::ActionContext::default(),
159 )?;
160 let definition = definition::<A>();
161 let prepared = prepare_on(
162 &mut host,
163 definition,
164 INSTANCE,
165 None,
166 ComponentRunnerInputs::from_native_process(),
167 )?;
168 let component = definition.instantiate(prepared.context, prepared.target, prepared.restore)?;
169 let mut app = ui_core::App::from_component(component, prepared.runner_inputs)?;
170 let seen = drive(&mut app);
171 app.close_components();
172 Ok(seen)
173}
174
175pub fn window_record<A: ResourceApp>(
180 instance: u64,
181 actions: &[<A::Resource as Resource>::Action],
182) -> Result<tilo_resource::Doc, LaunchError> {
183 let mut doc = tilo_resource::Doc::new();
184 let kind = definition::<A>().record_kind.clone();
185 if !ui_core::record::init_scoped(
186 &mut doc,
187 &RecordId::window_default(instance),
188 &kind,
189 TEST_WORKSPACE,
190 Some(TEST_WORKSPACE),
191 ) {
192 return Err(operation("the window record could not be stamped"));
193 }
194 <A::Resource as Resource>::initialize(&mut doc).map_err(operation)?;
195 for action in actions {
196 tilo_actions::Action::apply(action, &mut doc).map_err(operation)?;
197 }
198 Ok(doc)
199}
200
201#[cfg(feature = "native")]
207pub fn drive_live_embedded<A: ResourceApp, T>(
208 drive: impl FnOnce(&mut ui_core::app::EmbeddedApp) -> T,
209) -> Result<T, LaunchError> {
210 let profile = profile_directory().map_err(operation)?;
211 let mut host = ApplicationHost::new(
212 profile.path().join("state"),
213 "test-device".into(),
214 "test-workspace",
215 UserId(AUTHOR.into()),
216 role_permissions(AUTHOR, Role::Owner),
217 tilo_actions::ActionContext::default(),
218 )?;
219 let definition = definition::<A>();
220 let prepared = prepare_on(
221 &mut host,
222 definition,
223 INSTANCE,
224 None,
225 ComponentRunnerInputs::from_native_process(),
226 )?;
227 let component = definition.instantiate(prepared.context, prepared.target, prepared.restore)?;
228 let app = ui_core::App::from_component(component, prepared.runner_inputs)?;
229 let mut tile = ui_core::app::EmbeddedApp::new(app, |_| {})?;
230 let seen = drive(&mut tile);
231 tile.app_mut().close_components();
232 Ok(seen)
233}
234
235pub fn dispatcher<R: Resource>(app_id: &'static str) -> super::Dispatcher<R> {
241 super::Dispatcher::new(app_id, R::KIND, false, Some(AUTHOR))
242}
243
244pub fn resolve<R: Resource>(
252 dispatcher: &super::Dispatcher<R>,
253 doc: &mut tilo_resource::Doc,
254) -> Result<Vec<Vec<R::Action>>, String> {
255 let mut applied = Vec::new();
256 let mut any_refused = false;
257 while let Some((invocation, _)) = dispatcher.core().take_attributed() {
258 if invocation.address.resource != dispatcher.core().resource_id() {
259 return Err(format!(
260 "`{}` on `{}` is not an action of {}",
261 invocation.address.action,
262 invocation.address.resource,
263 R::LABEL
264 ));
265 }
266 let now_ms = tilo_resource::origin_now_ms();
267 let actions = if invocation.address.action == tilo_resource::GROUP {
268 tilo_resource::group::build_group::<R>(&invocation.arguments, now_ms)?.actions
269 } else {
270 vec![tilo_resource::build_action::<R>(
271 &invocation.address.action,
272 &invocation.arguments,
273 now_ms,
274 )?]
275 };
276 let mut refused = None;
277 for action in &actions {
278 if let Err(error) =
279 dispatcher.apply_confirmed(&mut ui_core::widget::Edits::lend(doc), action)
280 {
281 refused = Some(error.to_string());
282 break;
283 }
284 }
285 match refused {
286 Some(reason) => {
287 any_refused = true;
288 dispatcher.core().refuse(&invocation.address.action, reason);
289 }
290 None => applied.push(actions),
291 }
292 }
293 for text in dispatcher.core().take_notices() {
296 if !any_refused {
297 dispatcher.core().kept(&text);
298 }
299 }
300 Ok(applied)
301}
302
303pub fn search_source<R: Resource>(
307 entries: Vec<tilo_resource::SearchEntry>,
308) -> impl ui_core::search::SearchProvider {
309 super::root::ResourceSearch::new(
310 R::LABEL,
311 R::APP_ID,
312 std::sync::Arc::new(super::root::IndexedEntries::built(entries, 0)),
313 )
314}