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>(
58 mut open: impl FnMut() -> Result<HeadlessApp<A>, LaunchError>,
59) -> Result<(), String> {
60 for viewport in SIDEBAR_BUTTON_WINDOWS {
61 let mut app = open().map_err(|error| format!("{viewport:?}: the app opens: {error}"))?;
62 let harness = app.harness();
63 harness
64 .resize(viewport)
65 .map_err(|error| format!("{viewport:?}: {error}"))?;
66 harness.present_in_window_bar();
67 harness
68 .check_window_sidebar_button()
69 .map_err(|error| format!("{viewport:?}: {error}"))?;
70 }
71 Ok(())
72}
73
74pub fn with_surface<A: ResourceApp, S: 'static, T>(
80 harness: &mut FrameHarness,
81 visit: impl FnOnce(&mut S) -> T,
82) -> Option<T> {
83 harness
84 .with_widget_mut(|root: &mut ResourceRoot| surface_of::<A, S>(root).map(visit))
85 .flatten()
86}
87
88pub fn surface_in_root<A: ResourceApp, S: 'static>(
92 widget: &mut dyn ui_core::widget::Widget,
93) -> Option<&mut S> {
94 surface_of::<A, S>(widget.as_any_mut()?.downcast_mut::<ResourceRoot>()?)
95}
96
97pub fn detached_dispatcher<R: Resource>(author: &str) -> super::Dispatcher<R> {
105 super::Dispatcher::new("test", R::KIND, false, Some(author))
106}
107
108pub fn detached_live<R: Resource>(
112 doc: &tilo_resource::Doc,
113) -> Result<super::Live<R>, tilo_resource::ModelError> {
114 Ok(super::Live::new(tilo_resource::Reader::read(doc)?))
115}
116
117pub fn inspect_live_window<A: ResourceApp, T>(
122 inspect: impl FnOnce(&ui_core::App) -> T,
123) -> Result<T, LaunchError> {
124 drive_live_window::<A, T>(|app| inspect(app))
125}
126
127pub fn drive_live_window<A: ResourceApp, T>(
131 drive: impl FnOnce(&mut ui_core::App) -> T,
132) -> Result<T, LaunchError> {
133 drive_live_window_writing_after::<A, T>(app_state::DEFAULT_DEBOUNCE, drive)
134}
135
136pub fn drive_live_window_writing_after<A: ResourceApp, T>(
143 debounce: ui_core::time::Duration,
144 drive: impl FnOnce(&mut ui_core::App) -> T,
145) -> Result<T, LaunchError> {
146 let profile = profile_directory().map_err(operation)?;
147 let mut host = ApplicationHost::with_store(
148 std::rc::Rc::new(app_state::FsStore::with_debounce(
149 profile.path().join("state"),
150 debounce,
151 )),
152 "test-device".into(),
153 "test-workspace",
154 UserId(AUTHOR.into()),
155 role_permissions(AUTHOR, Role::Owner),
156 tilo_actions::ActionContext::default(),
157 )?;
158 let definition = definition::<A>();
159 let prepared = prepare_on(
160 &mut host,
161 definition,
162 INSTANCE,
163 None,
164 ComponentRunnerInputs::from_native_process(),
165 )?;
166 let component = definition.instantiate(prepared.context, prepared.target, prepared.restore)?;
167 let mut app = ui_core::App::from_component(component, prepared.runner_inputs)?;
168 let seen = drive(&mut app);
169 app.close_components();
170 Ok(seen)
171}
172
173pub fn window_record<A: ResourceApp>(
178 instance: u64,
179 actions: &[<A::Resource as Resource>::Action],
180) -> Result<tilo_resource::Doc, LaunchError> {
181 let mut doc = tilo_resource::Doc::new();
182 let kind = definition::<A>().record_kind.clone();
183 if !ui_core::record::init_scoped(
184 &mut doc,
185 &RecordId::window_default(instance),
186 &kind,
187 TEST_WORKSPACE,
188 Some(TEST_WORKSPACE),
189 ) {
190 return Err(operation("the window record could not be stamped"));
191 }
192 <A::Resource as Resource>::initialize(&mut doc).map_err(operation)?;
193 for action in actions {
194 tilo_actions::Action::apply(action, &mut doc).map_err(operation)?;
195 }
196 Ok(doc)
197}
198
199#[cfg(feature = "native")]
205pub fn drive_live_embedded<A: ResourceApp, T>(
206 drive: impl FnOnce(&mut ui_core::app::EmbeddedApp) -> T,
207) -> Result<T, LaunchError> {
208 let profile = profile_directory().map_err(operation)?;
209 let mut host = ApplicationHost::new(
210 profile.path().join("state"),
211 "test-device".into(),
212 "test-workspace",
213 UserId(AUTHOR.into()),
214 role_permissions(AUTHOR, Role::Owner),
215 tilo_actions::ActionContext::default(),
216 )?;
217 let definition = definition::<A>();
218 let prepared = prepare_on(
219 &mut host,
220 definition,
221 INSTANCE,
222 None,
223 ComponentRunnerInputs::from_native_process(),
224 )?;
225 let component = definition.instantiate(prepared.context, prepared.target, prepared.restore)?;
226 let app = ui_core::App::from_component(component, prepared.runner_inputs)?;
227 let mut tile = ui_core::app::EmbeddedApp::new(app, |_| {})?;
228 let seen = drive(&mut tile);
229 tile.app_mut().close_components();
230 Ok(seen)
231}
232
233pub fn dispatcher<R: Resource>(app_id: &'static str) -> super::Dispatcher<R> {
239 super::Dispatcher::new(app_id, R::KIND, false, Some(AUTHOR))
240}
241
242pub fn resolve<R: Resource>(
250 dispatcher: &super::Dispatcher<R>,
251 doc: &mut tilo_resource::Doc,
252) -> Result<Vec<Vec<R::Action>>, String> {
253 let mut applied = Vec::new();
254 let mut any_refused = false;
255 while let Some((invocation, _)) = dispatcher.core().take_attributed() {
256 if invocation.address.resource != dispatcher.core().resource_id() {
257 return Err(format!(
258 "`{}` on `{}` is not an action of {}",
259 invocation.address.action,
260 invocation.address.resource,
261 R::LABEL
262 ));
263 }
264 let now_ms = tilo_resource::origin_now_ms();
265 let actions = if invocation.address.action == tilo_resource::GROUP {
266 tilo_resource::group::build_group::<R>(&invocation.arguments, now_ms)?.actions
267 } else {
268 vec![tilo_resource::build_action::<R>(
269 &invocation.address.action,
270 &invocation.arguments,
271 now_ms,
272 )?]
273 };
274 let mut refused = None;
275 for action in &actions {
276 if let Err(error) =
277 dispatcher.apply_confirmed(&mut ui_core::widget::Edits::lend(doc), action)
278 {
279 refused = Some(error.to_string());
280 break;
281 }
282 }
283 match refused {
284 Some(reason) => {
285 any_refused = true;
286 dispatcher.core().refuse(&invocation.address.action, reason);
287 }
288 None => applied.push(actions),
289 }
290 }
291 for text in dispatcher.core().take_notices() {
294 if !any_refused {
295 dispatcher.core().kept(&text);
296 }
297 }
298 Ok(applied)
299}
300
301pub fn search_source<R: Resource>(
305 entries: Vec<tilo_resource::SearchEntry>,
306) -> impl ui_core::search::SearchProvider {
307 super::root::ResourceSearch::new(
308 R::LABEL,
309 R::APP_ID,
310 std::sync::Arc::new(super::root::IndexedEntries::built(entries, 0)),
311 )
312}