1use std::{
6 borrow::Cow,
7 collections::HashMap,
8 fmt,
9 sync::{atomic::AtomicBool, Arc, Mutex, MutexGuard},
10};
11
12use serde::Serialize;
13use url::Url;
14
15use tauri_macros::default_runtime;
16use tauri_utils::{
17 assets::{AssetKey, CspHash, SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
18 config::{Csp, CspDirectiveSources},
19};
20
21use crate::{
22 app::{
23 AppHandle, ChannelInterceptor, GlobalWebviewEventListener, GlobalWindowEventListener,
24 OnPageLoad,
25 },
26 event::{EmitArgs, Event, EventId, EventTarget, Listeners},
27 ipc::{Invoke, InvokeHandler, RuntimeAuthority},
28 plugin::PluginStore,
29 resources::ResourceTable,
30 utils::{config::Config, PackageInfo},
31 Assets, Context, DebugAppIcon, EventName, Pattern, Runtime, StateManager, Webview, Window,
32};
33
34#[cfg(any(target_os = "macos", target_os = "ios"))]
35use crate::app::OnWebContentProcessTerminate;
36
37#[cfg(desktop)]
38mod menu;
39#[cfg(all(desktop, feature = "tray-icon"))]
40mod tray;
41pub mod webview;
42pub mod window;
43
44#[derive(Default)]
45struct CspHashStrings {
47 script: Vec<String>,
48 style: Vec<String>,
49}
50
51pub(crate) fn set_csp<R: Runtime>(
54 asset: &mut String,
55 assets: &impl std::borrow::Borrow<dyn Assets<R>>,
56 asset_path: &AssetKey,
57 manager: &AppManager<R>,
58 csp: Csp,
59) -> HashMap<String, CspDirectiveSources> {
60 let mut csp = csp.into();
61 let hash_strings =
62 assets
63 .borrow()
64 .csp_hashes(asset_path)
65 .fold(CspHashStrings::default(), |mut acc, hash| {
66 match hash {
67 CspHash::Script(hash) => {
68 acc.script.push(hash.into());
69 }
70 CspHash::Style(hash) => {
71 acc.style.push(hash.into());
72 }
73 _csp_hash => {
74 log::debug!("Unknown CspHash variant encountered: {:?}", _csp_hash);
75 }
76 }
77
78 acc
79 });
80
81 let dangerous_disable_asset_csp_modification = &manager
82 .config()
83 .app
84 .security
85 .dangerous_disable_asset_csp_modification;
86 if dangerous_disable_asset_csp_modification.can_modify("script-src") {
87 replace_csp_nonce(
88 asset,
89 SCRIPT_NONCE_TOKEN,
90 &mut csp,
91 "script-src",
92 hash_strings.script,
93 );
94 }
95
96 if dangerous_disable_asset_csp_modification.can_modify("style-src") {
97 replace_csp_nonce(
98 asset,
99 STYLE_NONCE_TOKEN,
100 &mut csp,
101 "style-src",
102 hash_strings.style,
103 );
104 }
105
106 csp
107}
108
109fn replace_with_callback<F: FnMut() -> String>(
111 original: &str,
112 pattern: &str,
113 mut replacement: F,
114) -> String {
115 let mut result = String::new();
116 let mut last_end = 0;
117 for (start, part) in original.match_indices(pattern) {
118 result.push_str(unsafe { original.get_unchecked(last_end..start) });
119 result.push_str(&replacement());
120 last_end = start + part.len();
121 }
122 result.push_str(unsafe { original.get_unchecked(last_end..original.len()) });
123 result
124}
125
126fn replace_csp_nonce(
127 asset: &mut String,
128 token: &str,
129 csp: &mut HashMap<String, CspDirectiveSources>,
130 directive: &str,
131 hashes: Vec<String>,
132) {
133 let mut nonces = Vec::new();
134 *asset = replace_with_callback(asset, token, || {
135 let nonce = getrandom::u64().expect("failed to get random bytes");
136 nonces.push(nonce);
137 nonce.to_string()
138 });
139
140 if !(nonces.is_empty() && hashes.is_empty()) {
141 let nonce_sources = nonces
142 .into_iter()
143 .map(|n| format!("'nonce-{n}'"))
144 .collect::<Vec<String>>();
145 let sources = csp.entry(directive.into()).or_default();
146 let self_source = "'self'".to_string();
147 if !sources.contains(&self_source) {
148 sources.push(self_source);
149 }
150 sources.extend(nonce_sources);
151 sources.extend(hashes);
152 }
153}
154
155#[non_exhaustive]
157pub struct Asset {
158 pub bytes: Vec<u8>,
160 pub mime_type: String,
162 pub csp_header: Option<String>,
164}
165
166impl Asset {
167 pub fn bytes(&self) -> &[u8] {
169 &self.bytes
170 }
171
172 pub fn mime_type(&self) -> &str {
174 &self.mime_type
175 }
176
177 pub fn csp_header(&self) -> Option<&str> {
179 self.csp_header.as_deref()
180 }
181}
182
183#[default_runtime(crate::Wry, wry)]
184pub struct AppManager<R: Runtime> {
185 pub runtime_authority: Mutex<RuntimeAuthority>,
186 pub window: window::WindowManager<R>,
187 pub webview: webview::WebviewManager<R>,
188 #[cfg(all(desktop, feature = "tray-icon"))]
189 pub tray: tray::TrayManager<R>,
190 #[cfg(desktop)]
191 pub menu: menu::MenuManager<R>,
192
193 pub(crate) plugins: Mutex<PluginStore<R>>,
194 pub listeners: Listeners,
195 pub state: Arc<StateManager>,
196 pub config: Config,
197 #[cfg(dev)]
198 pub config_parent: Option<std::path::PathBuf>,
199 pub assets: Box<dyn Assets<R>>,
200
201 pub app_icon: Option<Vec<u8>>,
202
203 pub package_info: PackageInfo,
204
205 pub pattern: Arc<Pattern>,
207
208 pub plugin_global_api_scripts: Arc<Option<&'static [&'static str]>>,
210
211 pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
213
214 pub(crate) invoke_key: String,
216
217 pub(crate) channel_interceptor: Option<ChannelInterceptor<R>>,
218
219 pub(crate) restart_on_exit: AtomicBool,
222}
223
224impl<R: Runtime> fmt::Debug for AppManager<R> {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 let mut d = f.debug_struct("AppManager");
227
228 d.field("window", &self.window)
229 .field("plugins", &self.plugins)
230 .field("state", &self.state)
231 .field("config", &self.config)
232 .field("app_icon", &DebugAppIcon(&self.app_icon))
233 .field("package_info", &self.package_info)
234 .field("pattern", &self.pattern);
235
236 #[cfg(all(desktop, feature = "tray-icon"))]
237 {
238 d.field("tray", &self.tray);
239 }
240
241 d.finish()
242 }
243}
244
245pub(crate) enum EmitPayload<'a, S: Serialize> {
246 Serialize(&'a S),
247 Str(String),
248}
249
250impl<R: Runtime> AppManager<R> {
251 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
252 pub(crate) fn with_handlers(
253 #[allow(unused_mut)] mut context: Context<R>,
254 plugins: PluginStore<R>,
255 invoke_handler: Box<InvokeHandler<R>>,
256 on_page_load: Option<Arc<OnPageLoad<R>>>,
257 #[cfg(any(target_os = "macos", target_os = "ios"))] on_web_content_process_terminate: Option<
258 Arc<OnWebContentProcessTerminate<R>>,
259 >,
260 uri_scheme_protocols: HashMap<String, Arc<webview::UriSchemeProtocol<R>>>,
261 state: StateManager,
262 #[cfg(desktop)] menu_event_listener: Vec<crate::app::GlobalMenuEventListener<AppHandle<R>>>,
263 #[cfg(all(desktop, feature = "tray-icon"))] tray_icon_event_listeners: Vec<
264 crate::app::GlobalTrayIconEventListener<AppHandle<R>>,
265 >,
266 window_event_listeners: Vec<GlobalWindowEventListener<R>>,
267 webview_event_listeners: Vec<GlobalWebviewEventListener<R>>,
268 #[cfg(desktop)] window_menu_event_listeners: HashMap<
269 String,
270 crate::app::GlobalMenuEventListener<Window<R>>,
271 >,
272 invoke_initialization_script: String,
273 channel_interceptor: Option<ChannelInterceptor<R>>,
274 invoke_key: String,
275 ) -> Self {
276 #[cfg(feature = "isolation")]
278 if let Pattern::Isolation { key, .. } = &mut context.pattern {
279 *key = uuid::Uuid::new_v4().to_string();
280 }
281
282 Self {
283 runtime_authority: Mutex::new(context.runtime_authority),
284 window: window::WindowManager {
285 windows: Mutex::default(),
286 default_icon: context.default_window_icon,
287 event_listeners: Arc::new(window_event_listeners),
288 },
289 webview: webview::WebviewManager {
290 webviews: Mutex::default(),
291 invoke_handler,
292 on_page_load,
293 #[cfg(any(target_os = "macos", target_os = "ios"))]
294 on_web_content_process_terminate,
295 uri_scheme_protocols: Mutex::new(uri_scheme_protocols),
296 event_listeners: Arc::new(webview_event_listeners),
297 invoke_initialization_script,
298 invoke_key: invoke_key.clone(),
299 },
300 #[cfg(all(desktop, feature = "tray-icon"))]
301 tray: tray::TrayManager {
302 icon: context.tray_icon,
303 icons: Default::default(),
304 global_event_listeners: Mutex::new(tray_icon_event_listeners),
305 event_listeners: Default::default(),
306 },
307 #[cfg(desktop)]
308 menu: menu::MenuManager {
309 menus: Default::default(),
310 menu: Default::default(),
311 global_event_listeners: Mutex::new(menu_event_listener),
312 event_listeners: Mutex::new(window_menu_event_listeners),
313 },
314 plugins: Mutex::new(plugins),
315 listeners: Listeners::default(),
316 state: Arc::new(state),
317 config: context.config,
318 #[cfg(dev)]
319 config_parent: context.config_parent,
320 assets: context.assets,
321 app_icon: context.app_icon,
322 package_info: context.package_info,
323 pattern: Arc::new(context.pattern),
324 plugin_global_api_scripts: Arc::new(context.plugin_global_api_scripts),
325 resources_table: Arc::default(),
326 invoke_key,
327 channel_interceptor,
328 restart_on_exit: AtomicBool::new(false),
329 }
330 }
331
332 pub(crate) fn state(&self) -> Arc<StateManager> {
334 self.state.clone()
335 }
336
337 pub(crate) fn tauri_protocol_url(&self, https: bool) -> Cow<'_, Url> {
340 if cfg!(windows) || cfg!(target_os = "android") {
341 let scheme = if https { "https" } else { "http" };
342 Cow::Owned(Url::parse(&format!("{scheme}://tauri.localhost")).unwrap())
343 } else {
344 Cow::Owned(Url::parse("tauri://localhost").unwrap())
345 }
346 }
347
348 pub(crate) fn get_app_url(&self, https: bool) -> Cow<'_, Url> {
354 #[cfg(dev)]
355 let url = self.config.build.dev_url.as_ref();
356 #[cfg(not(dev))]
357 let url = match self.config.build.frontend_dist.as_ref() {
358 Some(crate::utils::config::FrontendDist::Url(url)) => Some(url),
359 _ => None,
360 };
361
362 if let Some(url) = url {
363 Cow::Borrowed(url)
364 } else {
365 self.tauri_protocol_url(https)
366 }
367 }
368
369 fn csp(&self) -> Option<Csp> {
370 if !crate::is_dev() {
371 self.config.app.security.csp.clone()
372 } else {
373 self
374 .config
375 .app
376 .security
377 .dev_csp
378 .clone()
379 .or_else(|| self.config.app.security.csp.clone())
380 }
381 }
382
383 pub fn get_asset(
385 &self,
386 mut path: String,
387 _use_https_schema: bool,
388 ) -> Result<Asset, Box<dyn std::error::Error>> {
389 let assets = &self.assets;
390 if path.ends_with('/') {
391 path.pop();
392 }
393 path = percent_encoding::percent_decode(path.as_bytes())
394 .decode_utf8_lossy()
395 .to_string();
396 let path = if path.is_empty() {
397 "index.html".to_string()
399 } else {
400 path.strip_prefix('/').unwrap_or(path.as_str()).to_string()
402 };
403
404 let mut asset_path = AssetKey::from(path.as_str());
405
406 let asset_response = assets
407 .get(&asset_path)
408 .or_else(|| {
409 log::debug!("Asset `{path}` not found; fallback to {path}.html");
410 let fallback = format!("{path}.html").into();
411 let asset = assets.get(&fallback);
412 asset_path = fallback;
413 asset
414 })
415 .or_else(|| {
416 log::debug!("Asset `{path}` not found; fallback to {path}/index.html",);
417 let fallback = format!("{path}/index.html").into();
418 let asset = assets.get(&fallback);
419 asset_path = fallback;
420 asset
421 })
422 .or_else(|| {
423 log::debug!("Asset `{path}` not found; fallback to index.html");
424 let fallback = AssetKey::from("index.html");
425 let asset = assets.get(&fallback);
426 asset_path = fallback;
427 asset
428 })
429 .ok_or_else(|| {
430 let error = crate::Error::AssetNotFound(path.clone());
431 log::error!("{error}");
432 Box::new(error)
433 })?;
434
435 let mut csp_header = None;
436 let is_html = asset_path.as_ref().ends_with(".html");
437
438 let final_data = if is_html {
439 let mut asset = String::from_utf8_lossy(&asset_response).into_owned();
440 if let Some(csp) = self.csp() {
441 #[allow(unused_mut)]
442 let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp);
443 #[cfg(feature = "isolation")]
444 if let Pattern::Isolation { schema, .. } = &*self.pattern {
445 let default_src = csp_map.entry("default-src".to_owned()).or_default();
446 default_src.push(crate::pattern::format_real_schema(
447 schema,
448 _use_https_schema,
449 ));
450 }
451
452 csp_header.replace(Csp::DirectiveMap(csp_map).to_string());
453 }
454
455 asset.into_bytes()
456 } else {
457 asset_response.into_owned()
458 };
459 let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path);
460 Ok(Asset {
461 bytes: final_data,
462 mime_type,
463 csp_header,
464 })
465 }
466
467 pub(crate) fn listeners(&self) -> &Listeners {
468 &self.listeners
469 }
470
471 pub fn run_invoke_handler(&self, invoke: Invoke<R>) -> bool {
472 (self.webview.invoke_handler)(invoke)
473 }
474
475 pub fn extend_api(&self, plugin: &str, invoke: Invoke<R>) -> bool {
476 self
477 .plugins
478 .lock()
479 .expect("poisoned plugin store")
480 .extend_api(plugin, invoke)
481 }
482
483 pub fn initialize_plugins(&self, app: &AppHandle<R>) -> crate::Result<()> {
484 self
485 .plugins
486 .lock()
487 .expect("poisoned plugin store")
488 .initialize_all(app, &self.config.plugins)
489 }
490
491 pub fn config(&self) -> &Config {
492 &self.config
493 }
494
495 #[cfg(dev)]
496 pub fn config_parent(&self) -> Option<&std::path::PathBuf> {
497 self.config_parent.as_ref()
498 }
499
500 pub fn package_info(&self) -> &PackageInfo {
501 &self.package_info
502 }
503
504 pub(crate) fn listen<F: Fn(Event) + Send + 'static>(
507 &self,
508 event: EventName,
509 target: EventTarget,
510 handler: F,
511 ) -> EventId {
512 self.listeners().listen(event, target, handler)
513 }
514
515 pub(crate) fn once<F: FnOnce(Event) + Send + 'static>(
518 &self,
519 event: EventName,
520 target: EventTarget,
521 handler: F,
522 ) -> EventId {
523 self.listeners().once(event, target, handler)
524 }
525
526 pub fn unlisten(&self, id: EventId) {
527 self.listeners().unlisten(id)
528 }
529
530 #[cfg_attr(
531 feature = "tracing",
532 tracing::instrument("app::emit", skip(self, payload))
533 )]
534 pub(crate) fn emit<S: Serialize>(
535 &self,
536 event: EventName<&str>,
537 payload: EmitPayload<'_, S>,
538 ) -> crate::Result<()> {
539 #[cfg(feature = "tracing")]
540 let _span = tracing::debug_span!("emit::run").entered();
541 let emit_args = match payload {
542 EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?,
543 EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?,
544 };
545
546 let listeners = self.listeners();
547
548 listeners.emit_js(self.webview.webviews_lock().values(), &emit_args)?;
549 listeners.emit(emit_args)?;
550
551 Ok(())
552 }
553
554 #[cfg_attr(
555 feature = "tracing",
556 tracing::instrument("app::emit::filter", skip(self, payload, filter))
557 )]
558 pub(crate) fn emit_filter<S, F>(
559 &self,
560 event: EventName<&str>,
561 payload: EmitPayload<'_, S>,
562 filter: F,
563 ) -> crate::Result<()>
564 where
565 S: Serialize,
566 F: Fn(&EventTarget) -> bool,
567 {
568 #[cfg(feature = "tracing")]
569 let _span = tracing::debug_span!("emit::run").entered();
570 let emit_args = match payload {
571 EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?,
572 EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?,
573 };
574
575 let listeners = self.listeners();
576
577 listeners.emit_js_filter(
578 self.webview.webviews_lock().values(),
579 &emit_args,
580 Some(&filter),
581 )?;
582
583 listeners.emit_filter(emit_args, Some(filter))?;
584
585 Ok(())
586 }
587
588 #[cfg_attr(
589 feature = "tracing",
590 tracing::instrument("app::emit::to", skip(self, target, payload), fields(target))
591 )]
592 pub(crate) fn emit_to<S>(
593 &self,
594 target: EventTarget,
595 event: EventName<&str>,
596 payload: EmitPayload<'_, S>,
597 ) -> crate::Result<()>
598 where
599 S: Serialize,
600 {
601 #[cfg(feature = "tracing")]
602 tracing::Span::current().record("target", format!("{target:?}"));
603
604 fn filter_target(target: &EventTarget, candidate: &EventTarget) -> bool {
605 match target {
606 EventTarget::AnyLabel { label } => match candidate {
608 EventTarget::Window { label: l }
609 | EventTarget::Webview { label: l }
610 | EventTarget::WebviewWindow { label: l }
611 | EventTarget::AnyLabel { label: l } => l == label,
612 _ => false,
613 },
614 EventTarget::Window { label } => match candidate {
615 EventTarget::AnyLabel { label: l } | EventTarget::Window { label: l } => l == label,
616 _ => false,
617 },
618 EventTarget::Webview { label } => match candidate {
619 EventTarget::AnyLabel { label: l } | EventTarget::Webview { label: l } => l == label,
620 _ => false,
621 },
622 EventTarget::WebviewWindow { label } => match candidate {
623 EventTarget::AnyLabel { label: l } | EventTarget::WebviewWindow { label: l } => {
624 l == label
625 }
626 _ => false,
627 },
628 _ => target == candidate,
630 }
631 }
632
633 match target {
634 EventTarget::Any => self.emit(event, payload),
636 target => self.emit_filter(event, payload, |t| filter_target(&target, t)),
637 }
638 }
639
640 pub fn get_window(&self, label: &str) -> Option<Window<R>> {
641 self.window.windows_lock().get(label).cloned()
642 }
643
644 pub fn get_focused_window(&self) -> Option<Window<R>> {
645 self
646 .window
647 .windows_lock()
648 .iter()
649 .find(|w| w.1.is_focused().unwrap_or(false))
650 .map(|w| w.1.clone())
651 }
652
653 pub(crate) fn on_window_close(&self, label: &str) {
654 let window = self.window.windows_lock().remove(label);
655 if let Some(window) = window {
656 for webview in window.webviews() {
657 self.webview.webviews_lock().remove(webview.label());
658 }
659 }
660 }
661
662 #[cfg(desktop)]
663 pub(crate) fn on_webview_close(&self, label: &str) {
664 self.webview.webviews_lock().remove(label);
665 }
666
667 pub fn windows(&self) -> HashMap<String, Window<R>> {
668 self.window.windows_lock().clone()
669 }
670
671 pub fn get_webview(&self, label: &str) -> Option<Webview<R>> {
672 self.webview.webviews_lock().get(label).cloned()
673 }
674
675 pub fn webviews(&self) -> HashMap<String, Webview<R>> {
676 self.webview.webviews_lock().clone()
677 }
678
679 pub(crate) fn resources_table(&self) -> MutexGuard<'_, ResourceTable> {
680 self
681 .resources_table
682 .lock()
683 .expect("poisoned window manager")
684 }
685
686 pub(crate) fn invoke_key(&self) -> &str {
687 &self.invoke_key
688 }
689}
690
691#[cfg(desktop)]
692impl<R: Runtime> AppManager<R> {
693 pub fn remove_menu_from_stash_by_id(&self, id: Option<&crate::menu::MenuId>) {
694 if let Some(id) = id {
695 let is_used_by_a_window = self
696 .window
697 .windows_lock()
698 .values()
699 .any(|w| w.is_menu_in_use(id));
700 if !(self.menu.is_menu_in_use(id) || is_used_by_a_window) {
701 self.menu.menus_stash_lock().remove(id);
702 }
703 }
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::replace_with_callback;
710
711 #[test]
712 fn string_replace_with_callback() {
713 let mut tauri_index = 0;
714 #[allow(clippy::single_element_loop)]
715 for (src, pattern, replacement, result) in [(
716 "tauri is awesome, tauri is amazing",
717 "tauri",
718 || {
719 tauri_index += 1;
720 tauri_index.to_string()
721 },
722 "1 is awesome, 2 is amazing",
723 )] {
724 assert_eq!(replace_with_callback(src, pattern, replacement), result);
725 }
726 }
727}
728
729#[cfg(test)]
730mod test {
731 use std::{
732 sync::mpsc::{channel, Receiver, Sender},
733 time::Duration,
734 };
735
736 use crate::{
737 event::EventTarget,
738 generate_context,
739 plugin::PluginStore,
740 test::{mock_app, MockRuntime},
741 webview::WebviewBuilder,
742 window::WindowBuilder,
743 App, Emitter, Listener, Manager, StateManager, Webview, WebviewWindow, WebviewWindowBuilder,
744 Window, Wry,
745 };
746
747 use super::AppManager;
748
749 const APP_LISTEN_ID: &str = "App::listen";
750 const APP_LISTEN_ANY_ID: &str = "App::listen_any";
751 const WINDOW_LISTEN_ID: &str = "Window::listen";
752 const WINDOW_LISTEN_ANY_ID: &str = "Window::listen_any";
753 const WEBVIEW_LISTEN_ID: &str = "Webview::listen";
754 const WEBVIEW_LISTEN_ANY_ID: &str = "Webview::listen_any";
755 const WEBVIEW_WINDOW_LISTEN_ID: &str = "WebviewWindow::listen";
756 const WEBVIEW_WINDOW_LISTEN_ANY_ID: &str = "WebviewWindow::listen_any";
757 const TEST_EVENT_NAME: &str = "event";
758
759 #[test]
760 fn check_get_url() {
761 let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true);
762 let manager: AppManager<Wry> = AppManager::with_handlers(
763 context,
764 PluginStore::default(),
765 Box::new(|_| false),
766 None,
767 #[cfg(any(target_os = "macos", target_os = "ios"))]
768 None,
769 Default::default(),
770 StateManager::new(),
771 Default::default(),
772 #[cfg(all(desktop, feature = "tray-icon"))]
773 Default::default(),
774 Default::default(),
775 Default::default(),
776 Default::default(),
777 "".into(),
778 None,
779 crate::generate_invoke_key().unwrap(),
780 );
781
782 #[cfg(custom_protocol)]
783 {
784 assert_eq!(
785 manager.get_app_url(false).to_string(),
786 if cfg!(windows) || cfg!(target_os = "android") {
787 "http://tauri.localhost/"
788 } else {
789 "tauri://localhost"
790 }
791 );
792 assert_eq!(
793 manager.get_app_url(true).to_string(),
794 if cfg!(windows) || cfg!(target_os = "android") {
795 "https://tauri.localhost/"
796 } else {
797 "tauri://localhost"
798 }
799 );
800 }
801
802 #[cfg(dev)]
803 assert_eq!(
804 manager.get_app_url(false).to_string(),
805 "http://localhost:4000/"
806 );
807 }
808
809 struct EventSetup {
810 app: App<MockRuntime>,
811 window: Window<MockRuntime>,
812 webview: Webview<MockRuntime>,
813 webview_window: WebviewWindow<MockRuntime>,
814 tx: Sender<(&'static str, String)>,
815 rx: Receiver<(&'static str, String)>,
816 }
817
818 fn setup_events(setup_any: bool) -> EventSetup {
819 let app = mock_app();
820
821 let window = WindowBuilder::new(&app, "main-window").build().unwrap();
822
823 let webview = window
824 .add_child(
825 WebviewBuilder::new("main-webview", Default::default()),
826 crate::LogicalPosition::new(0, 0),
827 window.inner_size().unwrap(),
828 )
829 .unwrap();
830
831 let webview_window = WebviewWindowBuilder::new(&app, "main-webview-window", Default::default())
832 .build()
833 .unwrap();
834
835 let (tx, rx) = channel();
836
837 macro_rules! setup_listener {
838 ($type:ident, $id:ident, $any_id:ident) => {
839 let tx_ = tx.clone();
840 $type.listen(TEST_EVENT_NAME, move |evt| {
841 tx_
842 .send(($id, serde_json::from_str::<String>(evt.payload()).unwrap()))
843 .unwrap();
844 });
845
846 if setup_any {
847 let tx_ = tx.clone();
848 $type.listen_any(TEST_EVENT_NAME, move |evt| {
849 tx_
850 .send((
851 $any_id,
852 serde_json::from_str::<String>(evt.payload()).unwrap(),
853 ))
854 .unwrap();
855 });
856 }
857 };
858 }
859
860 setup_listener!(app, APP_LISTEN_ID, APP_LISTEN_ANY_ID);
861 setup_listener!(window, WINDOW_LISTEN_ID, WINDOW_LISTEN_ANY_ID);
862 setup_listener!(webview, WEBVIEW_LISTEN_ID, WEBVIEW_LISTEN_ANY_ID);
863 setup_listener!(
864 webview_window,
865 WEBVIEW_WINDOW_LISTEN_ID,
866 WEBVIEW_WINDOW_LISTEN_ANY_ID
867 );
868
869 EventSetup {
870 app,
871 window,
872 webview,
873 webview_window,
874 tx,
875 rx,
876 }
877 }
878
879 fn assert_events(kind: &str, received: &[&str], expected: &[&str]) {
880 for e in expected {
881 assert!(received.contains(e), "{e} did not receive `{kind}` event");
882 }
883 assert_eq!(
884 received.len(),
885 expected.len(),
886 "received {received:?} `{kind}` events but expected {expected:?}"
887 );
888 }
889
890 #[test]
891 fn emit() {
892 let EventSetup {
893 app,
894 window,
895 webview,
896 webview_window,
897 tx: _,
898 rx,
899 } = setup_events(true);
900
901 run_emit_test("emit (app)", app, &rx);
902 run_emit_test("emit (window)", window, &rx);
903 run_emit_test("emit (webview)", webview, &rx);
904 run_emit_test("emit (webview_window)", webview_window, &rx);
905 }
906
907 fn run_emit_test<M: Manager<MockRuntime> + Emitter<MockRuntime>>(
908 kind: &str,
909 m: M,
910 rx: &Receiver<(&str, String)>,
911 ) {
912 let mut received = Vec::new();
913 let payload = "global-payload";
914 m.emit(TEST_EVENT_NAME, payload).unwrap();
915 while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
916 assert_eq!(p, payload);
917 received.push(source);
918 }
919 assert_events(
920 kind,
921 &received,
922 &[
923 APP_LISTEN_ID,
924 APP_LISTEN_ANY_ID,
925 WINDOW_LISTEN_ID,
926 WINDOW_LISTEN_ANY_ID,
927 WEBVIEW_LISTEN_ID,
928 WEBVIEW_LISTEN_ANY_ID,
929 WEBVIEW_WINDOW_LISTEN_ID,
930 WEBVIEW_WINDOW_LISTEN_ANY_ID,
931 ],
932 );
933 }
934
935 #[test]
936 fn emit_to() {
937 let EventSetup {
938 app,
939 window,
940 webview,
941 webview_window,
942 tx,
943 rx,
944 } = setup_events(false);
945
946 run_emit_to_test(
947 "emit_to (App)",
948 &app,
949 &window,
950 &webview,
951 &webview_window,
952 tx.clone(),
953 &rx,
954 );
955 run_emit_to_test(
956 "emit_to (window)",
957 &window,
958 &window,
959 &webview,
960 &webview_window,
961 tx.clone(),
962 &rx,
963 );
964 run_emit_to_test(
965 "emit_to (webview)",
966 &webview,
967 &window,
968 &webview,
969 &webview_window,
970 tx.clone(),
971 &rx,
972 );
973 run_emit_to_test(
974 "emit_to (webview_window)",
975 &webview_window,
976 &window,
977 &webview,
978 &webview_window,
979 tx.clone(),
980 &rx,
981 );
982 }
983
984 fn run_emit_to_test<M: Manager<MockRuntime> + Emitter<MockRuntime>>(
985 kind: &str,
986 m: &M,
987 window: &Window<MockRuntime>,
988 webview: &Webview<MockRuntime>,
989 webview_window: &WebviewWindow<MockRuntime>,
990 tx: Sender<(&'static str, String)>,
991 rx: &Receiver<(&'static str, String)>,
992 ) {
993 let mut received = Vec::new();
994 let payload = "global-payload";
995
996 macro_rules! test_target {
997 ($target:expr, $id:ident) => {
998 m.emit_to($target, TEST_EVENT_NAME, payload).unwrap();
999 while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
1000 assert_eq!(p, payload);
1001 received.push(source);
1002 }
1003 assert_events(kind, &received, &[$id]);
1004
1005 received.clear();
1006 };
1007 }
1008
1009 test_target!(EventTarget::App, APP_LISTEN_ID);
1010 test_target!(window.label(), WINDOW_LISTEN_ID);
1011 test_target!(webview.label(), WEBVIEW_LISTEN_ID);
1012 test_target!(webview_window.label(), WEBVIEW_WINDOW_LISTEN_ID);
1013
1014 let other_webview_listen_id = "OtherWebview::listen";
1015 let other_webview = WebviewWindowBuilder::new(
1016 window,
1017 kind.replace(['(', ')', ' '], ""),
1018 Default::default(),
1019 )
1020 .build()
1021 .unwrap();
1022
1023 other_webview.listen(TEST_EVENT_NAME, move |evt| {
1024 tx.send((
1025 other_webview_listen_id,
1026 serde_json::from_str::<String>(evt.payload()).unwrap(),
1027 ))
1028 .unwrap();
1029 });
1030 m.emit_to(other_webview.label(), TEST_EVENT_NAME, payload)
1031 .unwrap();
1032 while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
1033 assert_eq!(p, payload);
1034 received.push(source);
1035 }
1036 assert_events("emit_to", &received, &[other_webview_listen_id]);
1037 }
1038}