Skip to main content

tauri/manager/
mod.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use 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)]
45/// Spaced and quoted Content-Security-Policy hash values.
46struct CspHashStrings {
47  script: Vec<String>,
48  style: Vec<String>,
49}
50
51/// Sets the CSP value to the asset HTML if needed (on Linux).
52/// Returns the CSP string for access on the response header (on Windows and macOS).
53pub(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
109// inspired by <https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297>
110fn 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/// A resolved asset.
156#[non_exhaustive]
157pub struct Asset {
158  /// The asset bytes.
159  pub bytes: Vec<u8>,
160  /// The asset's mime type.
161  pub mime_type: String,
162  /// The `Content-Security-Policy` header value.
163  pub csp_header: Option<String>,
164}
165
166impl Asset {
167  /// The asset bytes.
168  pub fn bytes(&self) -> &[u8] {
169    &self.bytes
170  }
171
172  /// The asset's mime type.
173  pub fn mime_type(&self) -> &str {
174    &self.mime_type
175  }
176
177  /// The `Content-Security-Policy` header value.
178  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  /// Application pattern.
206  pub pattern: Arc<Pattern>,
207
208  /// Global API scripts collected from plugins.
209  pub plugin_global_api_scripts: Arc<Option<&'static [&'static str]>>,
210
211  /// Application Resources Table
212  pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
213
214  /// Runtime-generated invoke key.
215  pub(crate) invoke_key: String,
216
217  pub(crate) channel_interceptor: Option<ChannelInterceptor<R>>,
218
219  /// Sets to true in [`AppHandle::request_restart`] and [`AppHandle::restart`]
220  /// and we will call `restart` on the next `RuntimeRunEvent::Exit` event
221  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    // generate a random isolation key at runtime
277    #[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  /// State managed by the application.
333  pub(crate) fn state(&self) -> Arc<StateManager> {
334    self.state.clone()
335  }
336
337  /// The `tauri` custom protocol URL we use to serve the embedded assets.
338  /// Returns `tauri://localhost` or its `wry` workaround URL `http://tauri.localhost`/`https://tauri.localhost`
339  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  /// Get the base app URL for [`WebviewUrl::App`](tauri_utils::config::WebviewUrl::App).
349  ///
350  /// * In dev mode, this is the [`devUrl`](tauri_utils::config::BuildConfig::dev_url) configuration value if it exsits.
351  /// * In production mode, this is the [`frontendDist`](tauri_utils::config::BuildConfig::frontend_dist) configuration value if it's a [`FrontendDist::Url`](tauri_utils::config::FrontendDist::Url).
352  /// * Returns [`Self::tauri_protocol_url`] (e.g. `tauri://localhost`) otherwise.
353  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  // TODO: Change to return `crate::Result` here in v3
384  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      // if the url is `tauri://localhost`, we should load `index.html`
398      "index.html".to_string()
399    } else {
400      // skip the leading `/`, if it starts with one.
401      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  /// # Panics
505  /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_`
506  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  /// # Panics
516  /// Will panic if `event` contains characters other than alphanumeric, `-`, `/`, `:` and `_`
517  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        // if targeting any label, filter matching labels
607        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        // otherwise match same target
629        _ => target == candidate,
630      }
631    }
632
633    match target {
634      // if targeting all, emit to all using emit without filter
635      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}