mod.rs 23 KB

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