mod.rs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. borrow::Cow,
  6. collections::HashMap,
  7. fmt,
  8. sync::{Arc, Mutex, MutexGuard},
  9. };
  10. use serde::Serialize;
  11. use url::Url;
  12. use tauri_macros::default_runtime;
  13. use tauri_utils::{
  14. assets::{AssetKey, CspHash},
  15. config::{Csp, CspDirectiveSources},
  16. html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
  17. };
  18. use crate::{
  19. app::{AppHandle, GlobalWebviewEventListener, GlobalWindowEventListener, OnPageLoad},
  20. event::{assert_event_name_is_valid, Event, EventId, EventTarget, Listeners},
  21. ipc::{Invoke, InvokeHandler, InvokeResponder, RuntimeAuthority},
  22. plugin::PluginStore,
  23. utils::{config::Config, PackageInfo},
  24. Assets, Context, Pattern, Runtime, StateManager, Window,
  25. };
  26. use crate::{event::EmitArgs, resources::ResourceTable, Webview};
  27. #[cfg(desktop)]
  28. mod menu;
  29. #[cfg(all(desktop, feature = "tray-icon"))]
  30. mod tray;
  31. pub mod webview;
  32. pub mod window;
  33. #[derive(Default)]
  34. /// Spaced and quoted Content-Security-Policy hash values.
  35. struct CspHashStrings {
  36. script: Vec<String>,
  37. style: Vec<String>,
  38. }
  39. /// Sets the CSP value to the asset HTML if needed (on Linux).
  40. /// Returns the CSP string for access on the response header (on Windows and macOS).
  41. #[allow(clippy::borrowed_box)]
  42. pub(crate) fn set_csp<R: Runtime>(
  43. asset: &mut String,
  44. assets: &impl std::borrow::Borrow<dyn Assets<R>>,
  45. asset_path: &AssetKey,
  46. manager: &AppManager<R>,
  47. csp: Csp,
  48. ) -> HashMap<String, CspDirectiveSources> {
  49. let mut csp = csp.into();
  50. let hash_strings =
  51. assets
  52. .borrow()
  53. .csp_hashes(asset_path)
  54. .fold(CspHashStrings::default(), |mut acc, hash| {
  55. match hash {
  56. CspHash::Script(hash) => {
  57. acc.script.push(hash.into());
  58. }
  59. CspHash::Style(hash) => {
  60. acc.style.push(hash.into());
  61. }
  62. _csp_hash => {
  63. log::debug!("Unknown CspHash variant encountered: {:?}", _csp_hash);
  64. }
  65. }
  66. acc
  67. });
  68. let dangerous_disable_asset_csp_modification = &manager
  69. .config()
  70. .app
  71. .security
  72. .dangerous_disable_asset_csp_modification;
  73. if dangerous_disable_asset_csp_modification.can_modify("script-src") {
  74. replace_csp_nonce(
  75. asset,
  76. SCRIPT_NONCE_TOKEN,
  77. &mut csp,
  78. "script-src",
  79. hash_strings.script,
  80. );
  81. }
  82. if dangerous_disable_asset_csp_modification.can_modify("style-src") {
  83. replace_csp_nonce(
  84. asset,
  85. STYLE_NONCE_TOKEN,
  86. &mut csp,
  87. "style-src",
  88. hash_strings.style,
  89. );
  90. }
  91. csp
  92. }
  93. // inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297
  94. fn replace_with_callback<F: FnMut() -> String>(
  95. original: &str,
  96. pattern: &str,
  97. mut replacement: F,
  98. ) -> String {
  99. let mut result = String::new();
  100. let mut last_end = 0;
  101. for (start, part) in original.match_indices(pattern) {
  102. result.push_str(unsafe { original.get_unchecked(last_end..start) });
  103. result.push_str(&replacement());
  104. last_end = start + part.len();
  105. }
  106. result.push_str(unsafe { original.get_unchecked(last_end..original.len()) });
  107. result
  108. }
  109. fn replace_csp_nonce(
  110. asset: &mut String,
  111. token: &str,
  112. csp: &mut HashMap<String, CspDirectiveSources>,
  113. directive: &str,
  114. hashes: Vec<String>,
  115. ) {
  116. let mut nonces = Vec::new();
  117. *asset = replace_with_callback(asset, token, || {
  118. #[cfg(target_pointer_width = "64")]
  119. let mut raw = [0u8; 8];
  120. #[cfg(target_pointer_width = "32")]
  121. let mut raw = [0u8; 4];
  122. #[cfg(target_pointer_width = "16")]
  123. let mut raw = [0u8; 2];
  124. getrandom::getrandom(&mut raw).expect("failed to get random bytes");
  125. let nonce = usize::from_ne_bytes(raw);
  126. nonces.push(nonce);
  127. nonce.to_string()
  128. });
  129. if !(nonces.is_empty() && hashes.is_empty()) {
  130. let nonce_sources = nonces
  131. .into_iter()
  132. .map(|n| format!("'nonce-{n}'"))
  133. .collect::<Vec<String>>();
  134. let sources = csp.entry(directive.into()).or_default();
  135. let self_source = "'self'".to_string();
  136. if !sources.contains(&self_source) {
  137. sources.push(self_source);
  138. }
  139. sources.extend(nonce_sources);
  140. sources.extend(hashes);
  141. }
  142. }
  143. /// A resolved asset.
  144. pub struct Asset {
  145. /// The asset bytes.
  146. pub bytes: Vec<u8>,
  147. /// The asset's mime type.
  148. pub mime_type: String,
  149. /// The `Content-Security-Policy` header value.
  150. pub csp_header: Option<String>,
  151. }
  152. #[default_runtime(crate::Wry, wry)]
  153. pub struct AppManager<R: Runtime> {
  154. pub runtime_authority: Mutex<RuntimeAuthority>,
  155. pub window: window::WindowManager<R>,
  156. pub webview: webview::WebviewManager<R>,
  157. #[cfg(all(desktop, feature = "tray-icon"))]
  158. pub tray: tray::TrayManager<R>,
  159. #[cfg(desktop)]
  160. pub menu: menu::MenuManager<R>,
  161. pub(crate) plugins: Mutex<PluginStore<R>>,
  162. pub listeners: Listeners,
  163. pub state: Arc<StateManager>,
  164. pub config: Config,
  165. pub assets: Box<dyn Assets<R>>,
  166. pub app_icon: Option<Vec<u8>>,
  167. pub package_info: PackageInfo,
  168. /// Application pattern.
  169. pub pattern: Arc<Pattern>,
  170. /// Global API scripts collected from plugins.
  171. pub plugin_global_api_scripts: Arc<Option<&'static [&'static str]>>,
  172. /// Application Resources Table
  173. pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
  174. }
  175. impl<R: Runtime> fmt::Debug for AppManager<R> {
  176. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  177. let mut d = f.debug_struct("AppManager");
  178. d.field("window", &self.window)
  179. .field("plugins", &self.plugins)
  180. .field("state", &self.state)
  181. .field("config", &self.config)
  182. .field("app_icon", &self.app_icon)
  183. .field("package_info", &self.package_info)
  184. .field("pattern", &self.pattern);
  185. #[cfg(all(desktop, feature = "tray-icon"))]
  186. {
  187. d.field("tray", &self.tray);
  188. }
  189. d.finish()
  190. }
  191. }
  192. impl<R: Runtime> AppManager<R> {
  193. #[allow(clippy::too_many_arguments, clippy::type_complexity)]
  194. pub(crate) fn with_handlers(
  195. #[allow(unused_mut)] mut context: Context<R>,
  196. plugins: PluginStore<R>,
  197. invoke_handler: Box<InvokeHandler<R>>,
  198. on_page_load: Option<Arc<OnPageLoad<R>>>,
  199. uri_scheme_protocols: HashMap<String, Arc<webview::UriSchemeProtocol<R>>>,
  200. state: StateManager,
  201. window_event_listeners: Vec<GlobalWindowEventListener<R>>,
  202. webiew_event_listeners: Vec<GlobalWebviewEventListener<R>>,
  203. #[cfg(desktop)] window_menu_event_listeners: HashMap<
  204. String,
  205. crate::app::GlobalMenuEventListener<Window<R>>,
  206. >,
  207. (invoke_responder, invoke_initialization_script): (Option<Arc<InvokeResponder<R>>>, String),
  208. ) -> Self {
  209. // generate a random isolation key at runtime
  210. #[cfg(feature = "isolation")]
  211. if let Pattern::Isolation { ref mut key, .. } = &mut context.pattern {
  212. *key = uuid::Uuid::new_v4().to_string();
  213. }
  214. Self {
  215. runtime_authority: Mutex::new(context.runtime_authority),
  216. window: window::WindowManager {
  217. windows: Mutex::default(),
  218. default_icon: context.default_window_icon,
  219. event_listeners: Arc::new(window_event_listeners),
  220. },
  221. webview: webview::WebviewManager {
  222. webviews: Mutex::default(),
  223. invoke_handler,
  224. on_page_load,
  225. uri_scheme_protocols: Mutex::new(uri_scheme_protocols),
  226. event_listeners: Arc::new(webiew_event_listeners),
  227. invoke_responder,
  228. invoke_initialization_script,
  229. },
  230. #[cfg(all(desktop, feature = "tray-icon"))]
  231. tray: tray::TrayManager {
  232. icon: context.tray_icon,
  233. icons: Default::default(),
  234. global_event_listeners: Default::default(),
  235. event_listeners: Default::default(),
  236. },
  237. #[cfg(desktop)]
  238. menu: menu::MenuManager {
  239. menus: Default::default(),
  240. menu: Default::default(),
  241. global_event_listeners: Default::default(),
  242. event_listeners: Mutex::new(window_menu_event_listeners),
  243. },
  244. plugins: Mutex::new(plugins),
  245. listeners: Listeners::default(),
  246. state: Arc::new(state),
  247. config: context.config,
  248. assets: context.assets,
  249. app_icon: context.app_icon,
  250. package_info: context.package_info,
  251. pattern: Arc::new(context.pattern),
  252. plugin_global_api_scripts: Arc::new(context.plugin_global_api_scripts),
  253. resources_table: Arc::default(),
  254. }
  255. }
  256. /// State managed by the application.
  257. pub(crate) fn state(&self) -> Arc<StateManager> {
  258. self.state.clone()
  259. }
  260. /// Get the base path to serve data from.
  261. ///
  262. /// * In dev mode, this will be based on the `devUrl` configuration value.
  263. /// * Otherwise, this will be based on the `frontendDist` configuration value.
  264. #[cfg(not(dev))]
  265. fn base_path(&self) -> Option<&Url> {
  266. use crate::utils::config::FrontendDist;
  267. match self.config.build.frontend_dist.as_ref() {
  268. Some(FrontendDist::Url(url)) => Some(url),
  269. _ => None,
  270. }
  271. }
  272. #[cfg(dev)]
  273. fn base_path(&self) -> Option<&Url> {
  274. self.config.build.dev_url.as_ref()
  275. }
  276. pub(crate) fn protocol_url(&self) -> Cow<'_, Url> {
  277. if cfg!(windows) || cfg!(target_os = "android") {
  278. Cow::Owned(Url::parse("http://tauri.localhost").unwrap())
  279. } else {
  280. Cow::Owned(Url::parse("tauri://localhost").unwrap())
  281. }
  282. }
  283. /// Get the base URL to use for webview requests.
  284. ///
  285. /// In dev mode, this will be based on the `devUrl` configuration value.
  286. pub(crate) fn get_url(&self) -> Cow<'_, Url> {
  287. match self.base_path() {
  288. Some(url) => Cow::Borrowed(url),
  289. _ => self.protocol_url(),
  290. }
  291. }
  292. fn csp(&self) -> Option<Csp> {
  293. if !crate::dev() {
  294. self.config.app.security.csp.clone()
  295. } else {
  296. self
  297. .config
  298. .app
  299. .security
  300. .dev_csp
  301. .clone()
  302. .or_else(|| self.config.app.security.csp.clone())
  303. }
  304. }
  305. pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
  306. let assets = &self.assets;
  307. if path.ends_with('/') {
  308. path.pop();
  309. }
  310. path = percent_encoding::percent_decode(path.as_bytes())
  311. .decode_utf8_lossy()
  312. .to_string();
  313. let path = if path.is_empty() {
  314. // if the url is `tauri://localhost`, we should load `index.html`
  315. "index.html".to_string()
  316. } else {
  317. // skip leading `/`
  318. path.chars().skip(1).collect::<String>()
  319. };
  320. let mut asset_path = AssetKey::from(path.as_str());
  321. let asset_response = assets
  322. .get(&path.as_str().into())
  323. .or_else(|| {
  324. log::debug!("Asset `{path}` not found; fallback to {path}.html");
  325. let fallback = format!("{}.html", path.as_str()).into();
  326. let asset = assets.get(&fallback);
  327. asset_path = fallback;
  328. asset
  329. })
  330. .or_else(|| {
  331. log::debug!(
  332. "Asset `{}` not found; fallback to {}/index.html",
  333. path,
  334. path
  335. );
  336. let fallback = format!("{}/index.html", path.as_str()).into();
  337. let asset = assets.get(&fallback);
  338. asset_path = fallback;
  339. asset
  340. })
  341. .or_else(|| {
  342. log::debug!("Asset `{}` not found; fallback to index.html", path);
  343. let fallback = AssetKey::from("index.html");
  344. let asset = assets.get(&fallback);
  345. asset_path = fallback;
  346. asset
  347. })
  348. .ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
  349. .map(Cow::into_owned);
  350. let mut csp_header = None;
  351. let is_html = asset_path.as_ref().ends_with(".html");
  352. match asset_response {
  353. Ok(asset) => {
  354. let final_data = if is_html {
  355. let mut asset = String::from_utf8_lossy(&asset).into_owned();
  356. if let Some(csp) = self.csp() {
  357. #[allow(unused_mut)]
  358. let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp);
  359. #[cfg(feature = "isolation")]
  360. if let Pattern::Isolation { schema, .. } = &*self.pattern {
  361. let default_src = csp_map
  362. .entry("default-src".into())
  363. .or_insert_with(Default::default);
  364. default_src.push(crate::pattern::format_real_schema(schema));
  365. }
  366. csp_header.replace(Csp::DirectiveMap(csp_map).to_string());
  367. }
  368. asset.as_bytes().to_vec()
  369. } else {
  370. asset
  371. };
  372. let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path);
  373. Ok(Asset {
  374. bytes: final_data.to_vec(),
  375. mime_type,
  376. csp_header,
  377. })
  378. }
  379. Err(e) => {
  380. log::error!("{:?}", e);
  381. Err(Box::new(e))
  382. }
  383. }
  384. }
  385. pub(crate) fn listeners(&self) -> &Listeners {
  386. &self.listeners
  387. }
  388. pub fn run_invoke_handler(&self, invoke: Invoke<R>) -> bool {
  389. (self.webview.invoke_handler)(invoke)
  390. }
  391. pub fn extend_api(&self, plugin: &str, invoke: Invoke<R>) -> bool {
  392. self
  393. .plugins
  394. .lock()
  395. .expect("poisoned plugin store")
  396. .extend_api(plugin, invoke)
  397. }
  398. pub fn initialize_plugins(&self, app: &AppHandle<R>) -> crate::Result<()> {
  399. self
  400. .plugins
  401. .lock()
  402. .expect("poisoned plugin store")
  403. .initialize_all(app, &self.config.plugins)
  404. }
  405. pub fn config(&self) -> &Config {
  406. &self.config
  407. }
  408. pub fn package_info(&self) -> &PackageInfo {
  409. &self.package_info
  410. }
  411. pub fn listen<F: Fn(Event) + Send + 'static>(
  412. &self,
  413. event: String,
  414. target: EventTarget,
  415. handler: F,
  416. ) -> EventId {
  417. assert_event_name_is_valid(&event);
  418. self.listeners().listen(event, target, handler)
  419. }
  420. pub fn unlisten(&self, id: EventId) {
  421. self.listeners().unlisten(id)
  422. }
  423. pub fn once<F: FnOnce(Event) + Send + 'static>(
  424. &self,
  425. event: String,
  426. target: EventTarget,
  427. handler: F,
  428. ) -> EventId {
  429. assert_event_name_is_valid(&event);
  430. self.listeners().once(event, target, handler)
  431. }
  432. pub fn emit_filter<S, F>(&self, event: &str, payload: S, filter: F) -> crate::Result<()>
  433. where
  434. S: Serialize + Clone,
  435. F: Fn(&EventTarget) -> bool,
  436. {
  437. assert_event_name_is_valid(event);
  438. #[cfg(feature = "tracing")]
  439. let _span = tracing::debug_span!("emit::run").entered();
  440. let emit_args = EmitArgs::new(event, payload)?;
  441. let listeners = self.listeners();
  442. listeners.emit_js_filter(
  443. self.webview.webviews_lock().values(),
  444. event,
  445. &emit_args,
  446. Some(&filter),
  447. )?;
  448. listeners.emit_filter(emit_args, Some(filter))?;
  449. Ok(())
  450. }
  451. pub fn emit<S: Serialize + Clone>(&self, event: &str, payload: S) -> crate::Result<()> {
  452. assert_event_name_is_valid(event);
  453. #[cfg(feature = "tracing")]
  454. let _span = tracing::debug_span!("emit::run").entered();
  455. let emit_args = EmitArgs::new(event, payload)?;
  456. let listeners = self.listeners();
  457. listeners.emit_js(self.webview.webviews_lock().values(), event, &emit_args)?;
  458. listeners.emit(emit_args)?;
  459. Ok(())
  460. }
  461. pub fn get_window(&self, label: &str) -> Option<Window<R>> {
  462. self.window.windows_lock().get(label).cloned()
  463. }
  464. pub fn get_focused_window(&self) -> Option<Window<R>> {
  465. self
  466. .window
  467. .windows_lock()
  468. .iter()
  469. .find(|w| w.1.is_focused().unwrap_or(false))
  470. .map(|w| w.1.clone())
  471. }
  472. pub(crate) fn on_window_close(&self, label: &str) {
  473. let window = self.window.windows_lock().remove(label);
  474. if let Some(window) = window {
  475. for webview in window.webviews() {
  476. self.webview.webviews_lock().remove(webview.label());
  477. }
  478. }
  479. }
  480. pub(crate) fn on_webview_close(&self, label: &str) {
  481. self.webview.webviews_lock().remove(label);
  482. if let Ok(webview_labels_array) = serde_json::to_string(&self.webview.labels()) {
  483. let _ = self.webview.eval_script_all(format!(
  484. r#"(function () {{ const metadata = window.__TAURI_INTERNALS__.metadata; if (metadata != null) {{ metadata.webviews = {webview_labels_array}.map(function (label) {{ return {{ label: label }} }}) }} }})()"#,
  485. ));
  486. }
  487. }
  488. pub fn windows(&self) -> HashMap<String, Window<R>> {
  489. self.window.windows_lock().clone()
  490. }
  491. pub fn get_webview(&self, label: &str) -> Option<Webview<R>> {
  492. self.webview.webviews_lock().get(label).cloned()
  493. }
  494. pub fn webviews(&self) -> HashMap<String, Webview<R>> {
  495. self.webview.webviews_lock().clone()
  496. }
  497. pub(crate) fn resources_table(&self) -> MutexGuard<'_, ResourceTable> {
  498. self
  499. .resources_table
  500. .lock()
  501. .expect("poisoned window manager")
  502. }
  503. }
  504. #[cfg(desktop)]
  505. impl<R: Runtime> AppManager<R> {
  506. pub fn remove_menu_from_stash_by_id(&self, id: Option<&crate::menu::MenuId>) {
  507. if let Some(id) = id {
  508. let is_used_by_a_window = self
  509. .window
  510. .windows_lock()
  511. .values()
  512. .any(|w| w.is_menu_in_use(id));
  513. if !(self.menu.is_menu_in_use(id) || is_used_by_a_window) {
  514. self.menu.menus_stash_lock().remove(id);
  515. }
  516. }
  517. }
  518. }
  519. #[cfg(test)]
  520. mod tests {
  521. use super::replace_with_callback;
  522. #[test]
  523. fn string_replace_with_callback() {
  524. let mut tauri_index = 0;
  525. #[allow(clippy::single_element_loop)]
  526. for (src, pattern, replacement, result) in [(
  527. "tauri is awesome, tauri is amazing",
  528. "tauri",
  529. || {
  530. tauri_index += 1;
  531. tauri_index.to_string()
  532. },
  533. "1 is awesome, 2 is amazing",
  534. )] {
  535. assert_eq!(replace_with_callback(src, pattern, replacement), result);
  536. }
  537. }
  538. }
  539. #[cfg(test)]
  540. mod test {
  541. use std::{
  542. sync::mpsc::{channel, Receiver, Sender},
  543. time::Duration,
  544. };
  545. use crate::{
  546. event::EventTarget,
  547. generate_context,
  548. plugin::PluginStore,
  549. test::{mock_app, MockRuntime},
  550. webview::WebviewBuilder,
  551. window::WindowBuilder,
  552. App, Manager, StateManager, Webview, WebviewWindow, WebviewWindowBuilder, Window, Wry,
  553. };
  554. use super::AppManager;
  555. const APP_LISTEN_ID: &str = "App::listen";
  556. const APP_LISTEN_ANY_ID: &str = "App::listen_any";
  557. const WINDOW_LISTEN_ID: &str = "Window::listen";
  558. const WINDOW_LISTEN_ANY_ID: &str = "Window::listen_any";
  559. const WEBVIEW_LISTEN_ID: &str = "Webview::listen";
  560. const WEBVIEW_LISTEN_ANY_ID: &str = "Webview::listen_any";
  561. const WEBVIEW_WINDOW_LISTEN_ID: &str = "WebviewWindow::listen";
  562. const WEBVIEW_WINDOW_LISTEN_ANY_ID: &str = "WebviewWindow::listen_any";
  563. const TEST_EVENT_NAME: &str = "event";
  564. #[test]
  565. fn check_get_url() {
  566. let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate);
  567. let manager: AppManager<Wry> = AppManager::with_handlers(
  568. context,
  569. PluginStore::default(),
  570. Box::new(|_| false),
  571. None,
  572. Default::default(),
  573. StateManager::new(),
  574. Default::default(),
  575. Default::default(),
  576. Default::default(),
  577. (None, "".into()),
  578. );
  579. #[cfg(custom_protocol)]
  580. {
  581. assert_eq!(
  582. manager.get_url().to_string(),
  583. if cfg!(windows) || cfg!(target_os = "android") {
  584. "http://tauri.localhost/"
  585. } else {
  586. "tauri://localhost"
  587. }
  588. );
  589. }
  590. #[cfg(dev)]
  591. assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
  592. }
  593. struct EventSetup {
  594. app: App<MockRuntime>,
  595. window: Window<MockRuntime>,
  596. webview: Webview<MockRuntime>,
  597. webview_window: WebviewWindow<MockRuntime>,
  598. tx: Sender<(&'static str, String)>,
  599. rx: Receiver<(&'static str, String)>,
  600. }
  601. fn setup_events(setup_any: bool) -> EventSetup {
  602. let app = mock_app();
  603. let window = WindowBuilder::new(&app, "main-window").build().unwrap();
  604. let webview = window
  605. .add_child(
  606. WebviewBuilder::new("main-webview", Default::default()),
  607. crate::LogicalPosition::new(0, 0),
  608. window.inner_size().unwrap(),
  609. )
  610. .unwrap();
  611. let webview_window = WebviewWindowBuilder::new(&app, "main-webview-window", Default::default())
  612. .build()
  613. .unwrap();
  614. let (tx, rx) = channel();
  615. macro_rules! setup_listener {
  616. ($type:ident, $id:ident, $any_id:ident) => {
  617. let tx_ = tx.clone();
  618. $type.listen(TEST_EVENT_NAME, move |evt| {
  619. tx_
  620. .send(($id, serde_json::from_str::<String>(evt.payload()).unwrap()))
  621. .unwrap();
  622. });
  623. if setup_any {
  624. let tx_ = tx.clone();
  625. $type.listen_any(TEST_EVENT_NAME, move |evt| {
  626. tx_
  627. .send((
  628. $any_id,
  629. serde_json::from_str::<String>(evt.payload()).unwrap(),
  630. ))
  631. .unwrap();
  632. });
  633. }
  634. };
  635. }
  636. setup_listener!(app, APP_LISTEN_ID, APP_LISTEN_ANY_ID);
  637. setup_listener!(window, WINDOW_LISTEN_ID, WINDOW_LISTEN_ANY_ID);
  638. setup_listener!(webview, WEBVIEW_LISTEN_ID, WEBVIEW_LISTEN_ANY_ID);
  639. setup_listener!(
  640. webview_window,
  641. WEBVIEW_WINDOW_LISTEN_ID,
  642. WEBVIEW_WINDOW_LISTEN_ANY_ID
  643. );
  644. EventSetup {
  645. app,
  646. window,
  647. webview,
  648. webview_window,
  649. tx,
  650. rx,
  651. }
  652. }
  653. fn assert_events(kind: &str, received: &[&str], expected: &[&str]) {
  654. for e in expected {
  655. assert!(received.contains(e), "{e} did not receive `{kind}` event");
  656. }
  657. assert_eq!(
  658. received.len(),
  659. expected.len(),
  660. "received {:?} `{kind}` events but expected {:?}",
  661. received,
  662. expected
  663. );
  664. }
  665. #[test]
  666. fn emit() {
  667. let EventSetup {
  668. app,
  669. window,
  670. webview,
  671. webview_window,
  672. tx: _,
  673. rx,
  674. } = setup_events(true);
  675. run_emit_test("emit (app)", app, &rx);
  676. run_emit_test("emit (window)", window, &rx);
  677. run_emit_test("emit (webview)", webview, &rx);
  678. run_emit_test("emit (webview_window)", webview_window, &rx);
  679. }
  680. fn run_emit_test<M: Manager<MockRuntime>>(kind: &str, m: M, rx: &Receiver<(&str, String)>) {
  681. let mut received = Vec::new();
  682. let payload = "global-payload";
  683. m.emit(TEST_EVENT_NAME, payload).unwrap();
  684. while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
  685. assert_eq!(p, payload);
  686. received.push(source);
  687. }
  688. assert_events(
  689. kind,
  690. &received,
  691. &[
  692. APP_LISTEN_ID,
  693. APP_LISTEN_ANY_ID,
  694. WINDOW_LISTEN_ID,
  695. WINDOW_LISTEN_ANY_ID,
  696. WEBVIEW_LISTEN_ID,
  697. WEBVIEW_LISTEN_ANY_ID,
  698. WEBVIEW_WINDOW_LISTEN_ID,
  699. WEBVIEW_WINDOW_LISTEN_ANY_ID,
  700. ],
  701. );
  702. }
  703. #[test]
  704. fn emit_to() {
  705. let EventSetup {
  706. app,
  707. window,
  708. webview,
  709. webview_window,
  710. tx,
  711. rx,
  712. } = setup_events(false);
  713. run_emit_to_test(
  714. "emit_to (App)",
  715. &app,
  716. &window,
  717. &webview,
  718. &webview_window,
  719. tx.clone(),
  720. &rx,
  721. );
  722. run_emit_to_test(
  723. "emit_to (window)",
  724. &window,
  725. &window,
  726. &webview,
  727. &webview_window,
  728. tx.clone(),
  729. &rx,
  730. );
  731. run_emit_to_test(
  732. "emit_to (webview)",
  733. &webview,
  734. &window,
  735. &webview,
  736. &webview_window,
  737. tx.clone(),
  738. &rx,
  739. );
  740. run_emit_to_test(
  741. "emit_to (webview_window)",
  742. &webview_window,
  743. &window,
  744. &webview,
  745. &webview_window,
  746. tx.clone(),
  747. &rx,
  748. );
  749. }
  750. fn run_emit_to_test<M: Manager<MockRuntime>>(
  751. kind: &str,
  752. m: &M,
  753. window: &Window<MockRuntime>,
  754. webview: &Webview<MockRuntime>,
  755. webview_window: &WebviewWindow<MockRuntime>,
  756. tx: Sender<(&'static str, String)>,
  757. rx: &Receiver<(&'static str, String)>,
  758. ) {
  759. let mut received = Vec::new();
  760. let payload = "global-payload";
  761. macro_rules! test_target {
  762. ($target:expr, $id:ident) => {
  763. m.emit_to($target, TEST_EVENT_NAME, payload).unwrap();
  764. while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
  765. assert_eq!(p, payload);
  766. received.push(source);
  767. }
  768. assert_events(kind, &received, &[$id]);
  769. received.clear();
  770. };
  771. }
  772. test_target!(EventTarget::App, APP_LISTEN_ID);
  773. test_target!(window.label(), WINDOW_LISTEN_ID);
  774. test_target!(webview.label(), WEBVIEW_LISTEN_ID);
  775. test_target!(webview_window.label(), WEBVIEW_WINDOW_LISTEN_ID);
  776. let other_webview_listen_id = "OtherWebview::listen";
  777. let other_webview = WebviewWindowBuilder::new(
  778. window,
  779. kind.replace(['(', ')', ' '], ""),
  780. Default::default(),
  781. )
  782. .build()
  783. .unwrap();
  784. other_webview.listen(TEST_EVENT_NAME, move |evt| {
  785. tx.send((
  786. other_webview_listen_id,
  787. serde_json::from_str::<String>(evt.payload()).unwrap(),
  788. ))
  789. .unwrap();
  790. });
  791. m.emit_to(other_webview.label(), TEST_EVENT_NAME, payload)
  792. .unwrap();
  793. while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
  794. assert_eq!(p, payload);
  795. received.push(source);
  796. }
  797. assert_events("emit_to", &received, &[other_webview_listen_id]);
  798. }
  799. }