tauri/test/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
5//! Utilities for unit testing on Tauri applications.
6//!
7//! # Stability
8//!
9//! This module is unstable.
10//!
11//! # Examples
12//!
13//! ```rust
14//! use tauri::test::{mock_builder, mock_context, noop_assets};
15//!
16//! #[tauri::command]
17//! fn ping() -> &'static str {
18//! "pong"
19//! }
20//!
21//! fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
22//! builder
23//! .invoke_handler(tauri::generate_handler![ping])
24//! // remove the string argument to use your app's config file
25//! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
26//! .expect("failed to build app")
27//! }
28//!
29//! fn main() {
30//! // Use `tauri::Builder::default()` to use the default runtime rather than the `MockRuntime`;
31//! // let app = create_app(tauri::Builder::default());
32//! let app = create_app(mock_builder());
33//! let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap();
34//!
35//! // run the `ping` command and assert it returns `pong`
36//! let res = tauri::test::get_ipc_response(
37//! &webview,
38//! tauri::webview::InvokeRequest {
39//! cmd: "ping".into(),
40//! callback: tauri::ipc::CallbackFn(0),
41//! error: tauri::ipc::CallbackFn(1),
42//! // alternatively use "tauri://localhost"
43//! url: "http://tauri.localhost".parse().unwrap(),
44//! body: tauri::ipc::InvokeBody::default(),
45//! headers: Default::default(),
46//! invoke_key: tauri::test::INVOKE_KEY.to_string(),
47//! },
48//! ).map(|b| b.deserialize::<String>().unwrap());
49//! }
50//! ```
51
52#![allow(unused_variables)]
53
54mod mock_runtime;
55pub use mock_runtime::*;
56use serde::Serialize;
57use serialize_to_javascript::DefaultTemplate;
58
59use std::{borrow::Cow, collections::HashMap, fmt::Debug};
60
61use crate::{
62 ipc::{InvokeError, InvokeResponse, InvokeResponseBody},
63 webview::InvokeRequest,
64 App, Assets, Builder, Context, Pattern, Runtime, Webview,
65};
66use tauri_utils::{
67 acl::resolved::Resolved,
68 assets::{AssetKey, AssetsIter, CspHash},
69 config::{AppConfig, Config},
70};
71
72/// The invoke key used for tests.
73pub const INVOKE_KEY: &str = "__invoke-key__";
74
75/// An empty [`Assets`] implementation.
76pub struct NoopAsset {
77 assets: HashMap<String, Vec<u8>>,
78 csp_hashes: Vec<CspHash<'static>>,
79}
80
81impl<R: Runtime> Assets<R> for NoopAsset {
82 fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
83 None
84 }
85
86 fn iter(&self) -> Box<AssetsIter<'_>> {
87 Box::new(
88 self
89 .assets
90 .iter()
91 .map(|(k, b)| (Cow::Borrowed(k.as_str()), Cow::Borrowed(b.as_slice()))),
92 )
93 }
94
95 fn csp_hashes(&self, html_path: &AssetKey) -> Box<dyn Iterator<Item = CspHash<'_>> + '_> {
96 Box::new(self.csp_hashes.iter().copied())
97 }
98}
99
100/// Creates a new empty [`Assets`] implementation.
101pub fn noop_assets() -> NoopAsset {
102 NoopAsset {
103 assets: Default::default(),
104 csp_hashes: Default::default(),
105 }
106}
107
108/// Creates a new [`crate::Context`] for testing.
109pub fn mock_context<R: Runtime, A: Assets<R>>(assets: A) -> crate::Context<R> {
110 Context {
111 config: Config {
112 schema: None,
113 product_name: Default::default(),
114 main_binary_name: Default::default(),
115 version: Default::default(),
116 identifier: Default::default(),
117 app: AppConfig {
118 with_global_tauri: Default::default(),
119 windows: Vec::new(),
120 security: Default::default(),
121 tray_icon: None,
122 macos_private_api: false,
123 enable_gtk_app_id: false,
124 },
125 bundle: Default::default(),
126 build: Default::default(),
127 plugins: Default::default(),
128 },
129 assets: Box::new(assets),
130 default_window_icon: None,
131 app_icon: None,
132 #[cfg(all(desktop, feature = "tray-icon"))]
133 tray_icon: None,
134 package_info: crate::PackageInfo {
135 name: "test".into(),
136 version: "0.1.0".parse().unwrap(),
137 authors: "Tauri",
138 description: "Tauri test",
139 crate_name: "test",
140 },
141 pattern: Pattern::Brownfield,
142 runtime_authority: crate::runtime_authority!(Default::default(), Resolved::default()),
143 plugin_global_api_scripts: None,
144
145 #[cfg(dev)]
146 config_parent: None,
147 }
148}
149
150/// Creates a new [`Builder`] using the [`MockRuntime`].
151///
152/// To use a dummy [`Context`], see [`mock_app`].
153///
154/// # Examples
155///
156/// ```rust
157/// #[cfg(test)]
158/// fn do_something() {
159/// let app = tauri::test::mock_builder()
160/// // remove the string argument to use your app's config file
161/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
162/// .unwrap();
163/// }
164/// ```
165pub fn mock_builder() -> Builder<MockRuntime> {
166 let mut builder = Builder::<MockRuntime>::new().enable_macos_default_menu(false);
167
168 builder.invoke_initialization_script = crate::app::InvokeInitializationScript {
169 process_ipc_message_fn: crate::manager::webview::PROCESS_IPC_MESSAGE_FN,
170 os_name: std::env::consts::OS,
171 fetch_channel_data_command: crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND,
172 invoke_key: INVOKE_KEY,
173 }
174 .render_default(&Default::default())
175 .unwrap()
176 .into_string();
177
178 builder.invoke_key = INVOKE_KEY.to_string();
179
180 builder
181}
182
183/// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`].
184pub fn mock_app() -> App<MockRuntime> {
185 mock_builder().build(mock_context(noop_assets())).unwrap()
186}
187
188/// Executes the given IPC message and assert the response matches the expected value.
189///
190/// # Examples
191///
192/// ```rust
193/// use tauri::test::{mock_builder, mock_context, noop_assets};
194///
195/// #[tauri::command]
196/// fn ping() -> &'static str {
197/// "pong"
198/// }
199///
200/// fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
201/// builder
202/// .invoke_handler(tauri::generate_handler![ping])
203/// // remove the string argument to use your app's config file
204/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
205/// .expect("failed to build app")
206/// }
207///
208/// fn main() {
209/// let app = create_app(mock_builder());
210/// let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap();
211///
212/// // run the `ping` command and assert it returns `pong`
213/// tauri::test::assert_ipc_response(
214/// &webview,
215/// tauri::webview::InvokeRequest {
216/// cmd: "ping".into(),
217/// callback: tauri::ipc::CallbackFn(0),
218/// error: tauri::ipc::CallbackFn(1),
219/// url: "http://tauri.localhost".parse().unwrap(),
220/// body: tauri::ipc::InvokeBody::default(),
221/// headers: Default::default(),
222/// invoke_key: tauri::test::INVOKE_KEY.to_string(),
223/// },
224/// Ok("pong")
225/// );
226/// }
227/// ```
228pub fn assert_ipc_response<
229 T: Serialize + Debug + Send + Sync + 'static,
230 W: AsRef<Webview<MockRuntime>>,
231>(
232 webview: &W,
233 request: InvokeRequest,
234 expected: Result<T, T>,
235) {
236 let response =
237 get_ipc_response(webview, request).map(|b| b.deserialize::<serde_json::Value>().unwrap());
238 assert_eq!(
239 response,
240 expected
241 .map(|e| serde_json::to_value(e).unwrap())
242 .map_err(|e| serde_json::to_value(e).unwrap())
243 );
244}
245
246/// Executes the given IPC message and get the return value.
247///
248/// # Examples
249///
250/// ```rust
251/// use tauri::test::{mock_builder, mock_context, noop_assets};
252///
253/// #[tauri::command]
254/// fn ping() -> &'static str {
255/// "pong"
256/// }
257///
258/// fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
259/// builder
260/// .invoke_handler(tauri::generate_handler![ping])
261/// // remove the string argument to use your app's config file
262/// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
263/// .expect("failed to build app")
264/// }
265///
266/// fn main() {
267/// let app = create_app(mock_builder());
268/// let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap();
269///
270/// // run the `ping` command and assert it returns `pong`
271/// let res = tauri::test::get_ipc_response(
272/// &webview,
273/// tauri::webview::InvokeRequest {
274/// cmd: "ping".into(),
275/// callback: tauri::ipc::CallbackFn(0),
276/// error: tauri::ipc::CallbackFn(1),
277/// url: "http://tauri.localhost".parse().unwrap(),
278/// body: tauri::ipc::InvokeBody::default(),
279/// headers: Default::default(),
280/// invoke_key: tauri::test::INVOKE_KEY.to_string(),
281/// },
282/// );
283/// assert!(res.is_ok());
284/// assert_eq!(res.unwrap().deserialize::<String>().unwrap(), String::from("pong"));
285/// }
286///```
287pub fn get_ipc_response<W: AsRef<Webview<MockRuntime>>>(
288 webview: &W,
289 request: InvokeRequest,
290) -> Result<InvokeResponseBody, serde_json::Value> {
291 let (tx, rx) = std::sync::mpsc::sync_channel(1);
292 webview.as_ref().clone().on_message(
293 request,
294 Box::new(move |_window, _cmd, response, _callback, _error| {
295 tx.send(response).unwrap();
296 }),
297 );
298
299 let res = rx.recv().expect("Failed to receive result from command");
300 match res {
301 InvokeResponse::Ok(b) => Ok(b),
302 InvokeResponse::Err(InvokeError(v)) => Err(v),
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use std::time::Duration;
309
310 use super::mock_app;
311
312 #[test]
313 fn run_app() {
314 let app = mock_app();
315
316 let w = crate::WebviewWindowBuilder::new(&app, "main", Default::default())
317 .build()
318 .unwrap();
319
320 std::thread::spawn(move || {
321 std::thread::sleep(Duration::from_secs(1));
322 w.close().unwrap();
323 });
324
325 app.run(|_app, event| {
326 println!("{event:?}");
327 });
328 }
329}