mod.rs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. //! Utilities for unit testing on Tauri applications.
  5. //!
  6. //! # Stability
  7. //!
  8. //! This module is unstable.
  9. //!
  10. //! # Examples
  11. //!
  12. //! ```rust
  13. //! #[tauri::command]
  14. //! fn my_cmd() {}
  15. //!
  16. //! fn create_app<R: tauri::Runtime>(mut builder: tauri::Builder<R>) -> tauri::App<R> {
  17. //! builder
  18. //! .setup(|app| {
  19. //! // do something
  20. //! Ok(())
  21. //! })
  22. //! .invoke_handler(tauri::generate_handler![my_cmd])
  23. //! // remove the string argument on your app
  24. //! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
  25. //! .expect("failed to build app")
  26. //! }
  27. //!
  28. //! fn main() {
  29. //! let app = create_app(tauri::Builder::default());
  30. //! // app.run(|_handle, _event| {});
  31. //! }
  32. //!
  33. //! //#[cfg(test)]
  34. //! mod tests {
  35. //! use tauri::Manager;
  36. //! //#[cfg(test)]
  37. //! fn something() {
  38. //! let app = super::create_app(tauri::test::mock_builder());
  39. //! let window = app.get_window("main").unwrap();
  40. //! // do something with the app and window
  41. //! // in this case we'll run the my_cmd command with no arguments
  42. //! tauri::test::assert_ipc_response(
  43. //! &window,
  44. //! tauri::InvokePayload {
  45. //! cmd: "my_cmd".into(),
  46. //! tauri_module: None,
  47. //! callback: tauri::api::ipc::CallbackFn(0),
  48. //! error: tauri::api::ipc::CallbackFn(1),
  49. //! inner: serde_json::Value::Null,
  50. //! },
  51. //! Ok(())
  52. //! );
  53. //! }
  54. //! }
  55. //! ```
  56. #![allow(unused_variables)]
  57. mod mock_runtime;
  58. pub use mock_runtime::*;
  59. use serde::Serialize;
  60. use serde_json::Value as JsonValue;
  61. use std::{
  62. borrow::Cow,
  63. collections::HashMap,
  64. fmt::Debug,
  65. hash::{Hash, Hasher},
  66. sync::{
  67. mpsc::{channel, Sender},
  68. Arc, Mutex,
  69. },
  70. };
  71. use crate::hooks::window_invoke_responder;
  72. #[cfg(shell_scope)]
  73. use crate::ShellScopeConfig;
  74. use crate::{api::ipc::CallbackFn, App, Builder, Context, InvokePayload, Manager, Pattern, Window};
  75. use tauri_utils::{
  76. assets::{AssetKey, Assets, CspHash},
  77. config::{CliConfig, Config, PatternKind, TauriConfig},
  78. };
  79. #[derive(Eq, PartialEq)]
  80. struct IpcKey {
  81. callback: CallbackFn,
  82. error: CallbackFn,
  83. }
  84. impl Hash for IpcKey {
  85. fn hash<H: Hasher>(&self, state: &mut H) {
  86. self.callback.0.hash(state);
  87. self.error.0.hash(state);
  88. }
  89. }
  90. struct Ipc(Mutex<HashMap<IpcKey, Sender<std::result::Result<JsonValue, JsonValue>>>>);
  91. /// An empty [`Assets`] implementation.
  92. pub struct NoopAsset {
  93. csp_hashes: Vec<CspHash<'static>>,
  94. }
  95. impl Assets for NoopAsset {
  96. fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
  97. None
  98. }
  99. fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
  100. Box::new(self.csp_hashes.iter().copied())
  101. }
  102. }
  103. /// Creates a new empty [`Assets`] implementation.
  104. pub fn noop_assets() -> NoopAsset {
  105. NoopAsset {
  106. csp_hashes: Default::default(),
  107. }
  108. }
  109. /// Creates a new [`crate::Context`] for testing.
  110. pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
  111. Context {
  112. config: Config {
  113. schema: None,
  114. package: Default::default(),
  115. tauri: TauriConfig {
  116. pattern: PatternKind::Brownfield,
  117. windows: vec![Default::default()],
  118. cli: Some(CliConfig {
  119. description: None,
  120. long_description: None,
  121. before_help: None,
  122. after_help: None,
  123. args: None,
  124. subcommands: None,
  125. }),
  126. bundle: Default::default(),
  127. allowlist: Default::default(),
  128. security: Default::default(),
  129. updater: Default::default(),
  130. system_tray: None,
  131. macos_private_api: false,
  132. },
  133. build: Default::default(),
  134. plugins: Default::default(),
  135. },
  136. assets: Arc::new(assets),
  137. default_window_icon: None,
  138. app_icon: None,
  139. system_tray_icon: None,
  140. package_info: crate::PackageInfo {
  141. name: "test".into(),
  142. version: "0.1.0".parse().unwrap(),
  143. authors: "Tauri",
  144. description: "Tauri test",
  145. },
  146. _info_plist: (),
  147. pattern: Pattern::Brownfield(std::marker::PhantomData),
  148. #[cfg(shell_scope)]
  149. shell_scope: ShellScopeConfig {
  150. open: None,
  151. scopes: HashMap::new(),
  152. },
  153. }
  154. }
  155. /// Creates a new [`Builder`] using the [`MockRuntime`].
  156. ///
  157. /// To use a dummy [`Context`], see [`mock_app`].
  158. ///
  159. /// # Examples
  160. ///
  161. /// ```rust
  162. /// #[cfg(test)]
  163. /// fn do_something() {
  164. /// let app = tauri::test::mock_builder()
  165. /// // remove the string argument to use your app's config file
  166. /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
  167. /// .unwrap();
  168. /// }
  169. /// ```
  170. pub fn mock_builder() -> Builder<MockRuntime> {
  171. let mut builder = Builder::<MockRuntime>::new().manage(Ipc(Default::default()));
  172. builder.invoke_responder = Arc::new(|window, response, callback, error| {
  173. let window_ = window.clone();
  174. let ipc = window_.state::<Ipc>();
  175. let mut ipc_ = ipc.0.lock().unwrap();
  176. if let Some(tx) = ipc_.remove(&IpcKey { callback, error }) {
  177. tx.send(response.into_result()).unwrap();
  178. } else {
  179. window_invoke_responder(window, response, callback, error)
  180. }
  181. });
  182. builder
  183. }
  184. /// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`].
  185. pub fn mock_app() -> App<MockRuntime> {
  186. mock_builder().build(mock_context(noop_assets())).unwrap()
  187. }
  188. /// Executes the given IPC message and assert the response matches the expected value.
  189. ///
  190. /// # Examples
  191. ///
  192. /// ```rust
  193. /// #[tauri::command]
  194. /// fn ping() -> &'static str {
  195. /// "pong"
  196. /// }
  197. ///
  198. /// fn create_app<R: tauri::Runtime>(mut builder: tauri::Builder<R>) -> tauri::App<R> {
  199. /// builder
  200. /// .invoke_handler(tauri::generate_handler![ping])
  201. /// // remove the string argument on your app
  202. /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
  203. /// .expect("failed to build app")
  204. /// }
  205. ///
  206. /// fn main() {
  207. /// let app = create_app(tauri::Builder::default());
  208. /// // app.run(|_handle, _event| {});}
  209. /// }
  210. ///
  211. /// //#[cfg(test)]
  212. /// mod tests {
  213. /// use tauri::Manager;
  214. ///
  215. /// //#[cfg(test)]
  216. /// fn something() {
  217. /// let app = super::create_app(tauri::test::mock_builder());
  218. /// let window = app.get_window("main").unwrap();
  219. ///
  220. /// // run the `ping` command and assert it returns `pong`
  221. /// tauri::test::assert_ipc_response(
  222. /// &window,
  223. /// tauri::InvokePayload {
  224. /// cmd: "ping".into(),
  225. /// tauri_module: None,
  226. /// callback: tauri::api::ipc::CallbackFn(0),
  227. /// error: tauri::api::ipc::CallbackFn(1),
  228. /// inner: serde_json::Value::Null,
  229. /// },
  230. /// // the expected response is a success with the "pong" payload
  231. /// // we could also use Err("error message") here to ensure the command failed
  232. /// Ok("pong")
  233. /// );
  234. /// }
  235. /// }
  236. /// ```
  237. pub fn assert_ipc_response<T: Serialize + Debug>(
  238. window: &Window<MockRuntime>,
  239. payload: InvokePayload,
  240. expected: Result<T, T>,
  241. ) {
  242. let callback = payload.callback;
  243. let error = payload.error;
  244. let ipc = window.state::<Ipc>();
  245. let (tx, rx) = channel();
  246. ipc.0.lock().unwrap().insert(IpcKey { callback, error }, tx);
  247. window.clone().on_message(payload).unwrap();
  248. assert_eq!(
  249. rx.recv().unwrap(),
  250. expected
  251. .map(|e| serde_json::to_value(e).unwrap())
  252. .map_err(|e| serde_json::to_value(e).unwrap())
  253. );
  254. }
  255. #[cfg(test)]
  256. pub(crate) fn mock_invoke_context() -> crate::endpoints::InvokeContext<MockRuntime> {
  257. let app = mock_app();
  258. crate::endpoints::InvokeContext {
  259. window: app.get_window("main").unwrap(),
  260. config: app.config(),
  261. package_info: app.package_info().clone(),
  262. }
  263. }
  264. #[cfg(test)]
  265. mod tests {
  266. use crate::Manager;
  267. use std::time::Duration;
  268. use super::mock_app;
  269. #[test]
  270. fn run_app() {
  271. let app = mock_app();
  272. let w = app.get_window("main").unwrap();
  273. std::thread::spawn(move || {
  274. std::thread::sleep(Duration::from_secs(1));
  275. w.close().unwrap();
  276. });
  277. app.run(|_app, event| {
  278. println!("{:?}", event);
  279. });
  280. }
  281. }