Skip to main content

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: if cfg!(any(windows, target_os = "android")) {
220///                 "http://tauri.localhost"
221///             } else {
222///                 "tauri://localhost"
223///             }.parse().unwrap(),
224///             body: tauri::ipc::InvokeBody::default(),
225///             headers: Default::default(),
226///             invoke_key: tauri::test::INVOKE_KEY.to_string(),
227///         },
228///       Ok("pong")
229///     );
230/// }
231/// ```
232pub fn assert_ipc_response<
233  T: Serialize + Debug + Send + Sync + 'static,
234  W: AsRef<Webview<MockRuntime>>,
235>(
236  webview: &W,
237  request: InvokeRequest,
238  expected: Result<T, T>,
239) {
240  let response =
241    get_ipc_response(webview, request).map(|b| b.deserialize::<serde_json::Value>().unwrap());
242  assert_eq!(
243    response,
244    expected
245      .map(|e| serde_json::to_value(e).unwrap())
246      .map_err(|e| serde_json::to_value(e).unwrap())
247  );
248}
249
250#[allow(clippy::needless_doctest_main)]
251/// Executes the given IPC message and get the return value.
252///
253/// # Examples
254///
255/// ```rust
256/// use tauri::test::{mock_builder, mock_context, noop_assets};
257///
258/// #[tauri::command]
259/// fn ping() -> &'static str {
260///     "pong"
261/// }
262///
263/// fn create_app<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::App<R> {
264///     builder
265///         .invoke_handler(tauri::generate_handler![ping])
266///         // remove the string argument to use your app's config file
267///         .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json"))
268///         .expect("failed to build app")
269/// }
270///
271/// fn main() {
272///     let app = create_app(mock_builder());
273///     let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()).build().unwrap();
274///
275///     // run the `ping` command and assert it returns `pong`
276///     let res = tauri::test::get_ipc_response(
277///         &webview,
278///         tauri::webview::InvokeRequest {
279///             cmd: "ping".into(),
280///             callback: tauri::ipc::CallbackFn(0),
281///             error: tauri::ipc::CallbackFn(1),
282///             url: if cfg!(any(windows, target_os = "android")) {
283///                 "http://tauri.localhost"
284///             } else {
285///                 "tauri://localhost"
286///             }.parse().unwrap(),
287///             body: tauri::ipc::InvokeBody::default(),
288///             headers: Default::default(),
289///             invoke_key: tauri::test::INVOKE_KEY.to_string(),
290///         },
291///     );
292///     assert!(res.is_ok());
293///     assert_eq!(res.unwrap().deserialize::<String>().unwrap(), String::from("pong"));
294/// }
295///```
296pub fn get_ipc_response<W: AsRef<Webview<MockRuntime>>>(
297  webview: &W,
298  request: InvokeRequest,
299) -> Result<InvokeResponseBody, serde_json::Value> {
300  let (tx, rx) = std::sync::mpsc::sync_channel(1);
301  webview.as_ref().clone().on_message(
302    request,
303    Box::new(move |_window, _cmd, response, _callback, _error| {
304      tx.send(response).unwrap();
305    }),
306  );
307
308  let res = rx.recv().expect("Failed to receive result from command");
309  match res {
310    InvokeResponse::Ok(b) => Ok(b),
311    InvokeResponse::Err(InvokeError(v)) => Err(v),
312  }
313}
314
315#[cfg(test)]
316mod tests {
317  use std::time::Duration;
318
319  use super::mock_app;
320
321  #[test]
322  fn run_app() {
323    let app = mock_app();
324
325    let w = crate::WebviewWindowBuilder::new(&app, "main", Default::default())
326      .build()
327      .unwrap();
328
329    std::thread::spawn(move || {
330      std::thread::sleep(Duration::from_secs(1));
331      w.close().unwrap();
332    });
333
334    app.run(|_app, event| {
335      println!("{event:?}");
336    });
337  }
338}