From 7ac442e3f617361fa98d156e72582972fab99ac5 Mon Sep 17 00:00:00 2001 From: Tensor-Programming Date: Tue, 7 Jan 2020 02:00:41 -0500 Subject: [PATCH] feat(core) add testing and refactor (#281) * update .gitignore * remove cargo.lock * refactor endpoints. * remove webview from fns * add testing * stop proptest from opening 100 tabs * add config * proptest for listen_fn * add tauri.js to fixture * add simple tests to runner * add setup_content unit test. * add proptest for check_server_url * add setup_port unit test. * update tauriresult to work with tiny_http * Revert "update tauriresult to work with tiny_http" This reverts commit 26f44b3bc10acb196bfdd07f932204163b02ffd7. * cleanup setup_content logic * add proc macros * add underscore * add proptest_config --- tauri/src/app/runner.rs | 77 +++++++ tauri/src/endpoints.rs | 295 ++++++++++++++++---------- tauri/src/lib.rs | 1 + tauri/test/fixture/src-tauri/tauri.js | 219 +++++++++++++++++++ 4 files changed, 483 insertions(+), 109 deletions(-) create mode 100644 tauri/test/fixture/src-tauri/tauri.js diff --git a/tauri/src/app/runner.rs b/tauri/src/app/runner.rs index 7977c7f5b..886880c9a 100644 --- a/tauri/src/app/runner.rs +++ b/tauri/src/app/runner.rs @@ -200,3 +200,80 @@ fn build_webview( .build()?, ) } + +#[cfg(test)] +mod test { + use proptest::prelude::*; + use web_view::Content; + + #[cfg(not(feature = "embedded-server"))] + use std::{fs::read_to_string, path::Path}; + + fn init_config() -> crate::config::Config { + crate::config::get().expect("unable to setup default config") + } + + #[test] + fn check_setup_content() { + let config = init_config(); + let _c = config.clone(); + + let res = super::setup_content(config); + + #[cfg(feature = "embedded-server")] + match res { + Ok(Content::Url(u)) => assert!(u.contains("http://")), + _ => assert!(false), + } + + #[cfg(feature = "no-server")] + match res { + Ok(Content::Html(s)) => assert_eq!( + s, + read_to_string(Path::new(env!("TAURI_DIST_DIR")).join("index.tauri.html")).unwrap() + ), + _ => assert!(false), + } + + #[cfg(not(any(feature = "embedded-server", feature = "no-server")))] + match res { + Ok(Content::Url(dp)) => assert_eq!(dp, _c.build.dev_path), + Ok(Content::Html(s)) => assert_eq!( + s, + read_to_string(Path::new(env!("TAURI_DIST_DIR")).join("index.tauri.html")).unwrap() + ), + _ => assert!(false), + } + } + + #[cfg(feature = "embedded-server")] + #[test] + fn check_setup_port() { + let config = init_config(); + + let res = super::setup_port(config); + match res { + Some((_s, _b)) => assert!(true), + _ => assert!(false), + } + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + #[cfg(feature = "embedded-server")] + #[test] + fn check_server_url(port in (any::().prop_map(|v| v.to_string()))) { + let config = init_config(); + let valid = true; + + let p = port.clone(); + + let res = super::setup_server_url(config, valid, port); + + match res { + Some(url) => assert!(url.contains(&p)), + None => assert!(false) + } + } + } +} diff --git a/tauri/src/endpoints.rs b/tauri/src/endpoints.rs index b0496498c..8319c9183 100644 --- a/tauri/src/endpoints.rs +++ b/tauri/src/endpoints.rs @@ -12,40 +12,7 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> Tau Ok(command) => { match command { Init {} => { - #[cfg(not(any(feature = "all-api", feature = "event")))] - let event_init = ""; - #[cfg(any(feature = "all-api", feature = "event"))] - let event_init = format!( - " - window['{queue}'] = []; - window['{fn}'] = function (payload, salt, ignoreQueue) {{ - const listeners = (window['{listeners}'] && window['{listeners}'][payload.type]) || [] - if (!ignoreQueue && listeners.length === 0) {{ - window['{queue}'].push({{ - payload: payload, - salt: salt - }}) - }} - - if (listeners.length > 0) {{ - window.tauri.promisified({{ - cmd: 'validateSalt', - salt: salt - }}).then(function () {{ - for (let i = listeners.length - 1; i >= 0; i--) {{ - const listener = listeners[i] - if (listener.once) - listeners.splice(i, 1) - listener.handler(payload) - }} - }}) - }} - }} - ", - fn = crate::event::emit_function_name(), - listeners = crate::event::event_listeners_object_name(), - queue = crate::event::event_queue_object_name() - ); + let event_init = init()?; webview.eval(&format!( r#"{event_init} window.external.invoke('{{"cmd":"__initialized"}}') @@ -109,11 +76,8 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> Tau } #[cfg(any(feature = "all-api", feature = "open"))] Open { uri } => { - crate::spawn(move || { - webbrowser::open(&uri).expect("Failed to open webbrowser with uri"); - }); + open_fn(uri)?; } - ValidateSalt { salt, callback, @@ -127,33 +91,8 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> Tau handler, once, } => { - webview - .eval(&format!( - " - if (window['{listeners}'] === void 0) {{ - window['{listeners}'] = {{}} - }} - if (window['{listeners}']['{evt}'] === void 0) {{ - window['{listeners}']['{evt}'] = [] - }} - window['{listeners}']['{evt}'].push({{ - handler: window['{handler}'], - once: {once_flag} - }}); - - for (let i = 0; i < (window['{queue}'] || []).length; i++) {{ - const e = window['{queue}'][i]; - window['{emit}'](e.payload, e.salt, true) - }} - ", - listeners = crate::event::event_listeners_object_name(), - queue = crate::event::event_queue_object_name(), - emit = crate::event::emit_function_name(), - evt = event, - handler = handler, - once_flag = if once { "true" } else { "false" } - )) - .expect("failed to call webview.eval from listen"); + let js_string = listen_fn(event, handler, once)?; + webview.eval(&js_string)?; } #[cfg(any(feature = "all-api", feature = "event"))] Emit { event, payload } => { @@ -166,53 +105,191 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> Tau callback, error, } => { - let handle = webview.handle(); - crate::execute_promise( - webview, - move || { - let read_asset = crate::assets::ASSETS.get(&format!( - "{}{}{}", - env!("TAURI_DIST_DIR"), - if asset.starts_with("/") { "" } else { "/" }, - asset - )); - if read_asset.is_err() { - return Err(r#""Asset not found""#.to_string()); - } - - if asset_type == "image" { - let ext = if asset.ends_with("gif") { - "gif" - } else if asset.ends_with("png") { - "png" - } else { - "jpeg" - }; - Ok(format!( - "`data:image/{};base64,{}`", - ext, - base64::encode(&read_asset.expect("Failed to read asset type").into_owned()) - )) - } else { - handle - .dispatch(move |_webview| { - _webview.eval( - &std::str::from_utf8( - &read_asset.expect("Failed to read asset type").into_owned(), - ) - .expect("failed to convert asset bytes to u8 slice"), - ) - }) - .map_err(|err| format!("`{}`", err)) - .map(|_| r#""Asset loaded successfully""#.to_string()) - } - }, - callback, - error, - ); + load_asset(webview, asset, asset_type, callback, error)?; } } Ok(true) } } } + +fn init() -> TauriResult { + #[cfg(not(any(feature = "all-api", feature = "event")))] + return Ok(String::from("")); + #[cfg(any(feature = "all-api", feature = "event"))] + return Ok(format!( + " + window['{queue}'] = []; + window['{fn}'] = function (payload, salt, ignoreQueue) {{ + const listeners = (window['{listeners}'] && window['{listeners}'][payload.type]) || [] + if (!ignoreQueue && listeners.length === 0) {{ + window['{queue}'].push({{ + payload: payload, + salt: salt + }}) + }} + + if (listeners.length > 0) {{ + window.tauri.promisified({{ + cmd: 'validateSalt', + salt: salt + }}).then(function () {{ + for (let i = listeners.length - 1; i >= 0; i--) {{ + const listener = listeners[i] + if (listener.once) + listeners.splice(i, 1) + listener.handler(payload) + }} + }}) + }} + }} + ", + fn = crate::event::emit_function_name(), + queue = crate::event::event_listeners_object_name(), + listeners = crate::event::event_queue_object_name(), + )); +} + +#[cfg(any(feature = "all-api", feature = "open"))] +fn open_fn(uri: String) -> TauriResult<()> { + crate::spawn(move || { + #[cfg(test)] + assert!(uri.contains("http://")); + + #[cfg(not(test))] + webbrowser::open(&uri).expect("Failed to open webbrowser with uri"); + }); + + Ok(()) +} + +#[cfg(any(feature = "all-api", feature = "event"))] +fn listen_fn(event: String, handler: String, once: bool) -> TauriResult { + Ok(format!( + "if (window['{listeners}'] === void 0) {{ + window['{listeners}'] = {{}} + }} + if (window['{listeners}']['{evt}'] === void 0) {{ + window['{listeners}']['{evt}'] = [] + }} + window['{listeners}']['{evt}'].push({{ + handler: window['{handler}'], + once: {once_flag} + }}); + + for (let i = 0; i < (window['{queue}'] || []).length; i++) {{ + const e = window['{queue}'][i]; + window['{emit}'](e.payload, e.salt, true) + }} + ", + listeners = crate::event::event_listeners_object_name(), + queue = crate::event::event_queue_object_name(), + emit = crate::event::emit_function_name(), + evt = event, + handler = handler, + once_flag = if once { "true" } else { "false" } + )) +} + +#[cfg(not(any(feature = "dev-server", feature = "embedded-server")))] +fn load_asset( + webview: &mut WebView<'_, T>, + asset: String, + asset_type: String, + callback: String, + error: String, +) -> TauriResult<()> { + let handle = webview.handle(); + crate::execute_promise( + webview, + move || { + let read_asset = crate::assets::ASSETS.get(&format!( + "{}{}{}", + env!("TAURI_DIST_DIR"), + if asset.starts_with("/") { "" } else { "/" }, + asset + )); + if read_asset.is_err() { + return Err(r#""Asset not found""#.to_string()); + } + + if asset_type == "image" { + let ext = if asset.ends_with("gif") { + "gif" + } else if asset.ends_with("png") { + "png" + } else { + "jpeg" + }; + Ok(format!( + "`data:image/{};base64,{}`", + ext, + base64::encode(&read_asset.expect("Failed to read asset type").into_owned()) + )) + } else { + handle + .dispatch(move |_webview| { + _webview.eval( + &std::str::from_utf8(&read_asset.expect("Failed to read asset type").into_owned()) + .expect("failed to convert asset bytes to u8 slice"), + ) + }) + .map_err(|err| format!("`{}`", err)) + .map(|_| r#""Asset loaded successfully""#.to_string()) + } + }, + callback, + error, + ); + + Ok(()) +} + +#[cfg(test)] +mod test { + use proptest::prelude::*; + + #[test] + // test to see if check init produces a string or not. + fn check_init() { + if cfg!(not(any(feature = "all-api", feature = "event"))) { + let res = super::init(); + match res { + Ok(s) => assert_eq!(s, ""), + Err(_) => assert!(false), + } + } else if cfg!(any(feature = "all-api", feature = "event")) { + let res = super::init(); + match res { + Ok(s) => assert!(s.contains("window.tauri.promisified")), + Err(_) => assert!(false), + } + } + } + + // check the listen_fn for various usecases. + proptest! { + #[cfg(any(feature = "all-api", feature = "event"))] + #[test] + fn check_listen_fn(event in "", handler in "", once in proptest::bool::ANY) { + let res = super::listen_fn(event, handler, once); + match res { + Ok(_) => assert!(true), + Err(_) => assert!(false) + } + } + } + + // Test the open func to see if proper uris can be opened by the browser. + proptest! { + #[cfg(any(feature = "all-api", feature = "open"))] + #[test] + fn check_open(uri in r"(http://)([\\w\\d\\.]+([\\w]{2,6})?)") { + let res = super::open_fn(uri); + match res { + Ok(_) => assert!(true), + Err(_) => assert!(false), + } + } + } +} diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index a29dc649f..0b4f7f5ac 100644 --- a/tauri/src/lib.rs +++ b/tauri/src/lib.rs @@ -87,6 +87,7 @@ mod test { use proptest::prelude::*; proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] #[test] // check to see if spawn executes a function. fn check_spawn_task(task in "[a-z]+") { diff --git a/tauri/test/fixture/src-tauri/tauri.js b/tauri/test/fixture/src-tauri/tauri.js new file mode 100644 index 000000000..d66898cd0 --- /dev/null +++ b/tauri/test/fixture/src-tauri/tauri.js @@ -0,0 +1,219 @@ +/* eslint-disable */ + +/** + * * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * Please whitelist these API functions in tauri.conf.json + * + **/ + +/** + * @module tauri + * @description This API interface makes powerful interactions available + * to be run on client side applications. They are opt-in features, and + * must be enabled in tauri.conf.json + * + * Each binding MUST provide these interfaces in order to be compliant, + * and also whitelist them based upon the developer's settings. + */ + +function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1) +} + +var uid = function () { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4() +} + +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + + +function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + + + + +var __reject = function () { + return new Promise(function (_, reject) { + reject(); + }); +} + +window.tauri = { + + invoke: function invoke(args) { + window.external.invoke(JSON.stringify(args)); + }, + + + listen: function listen(event, handler) { + + + return __reject() + + }, + + + emit: function emit(evt, payload) { + + + return __reject() + + }, + + + transformCallback: function transformCallback(callback) { + var once = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var identifier = Object.freeze(uid()); + + window[identifier] = function (result) { + if (once) { + delete window[identifier]; + } + + return callback && callback(result); + }; + + return identifier; + }, + + + promisified: function promisified(args) { + var _this = this; + + return new Promise(function (resolve, reject) { + _this.invoke(_objectSpread({ + callback: _this.transformCallback(resolve), + error: _this.transformCallback(reject) + }, args)); + }); + }, + + + readTextFile: function readTextFile(path) { + + + return __reject() + + }, + + + readBinaryFile: function readBinaryFile(path) { + + + return __reject() + + }, + + + writeFile: function writeFile(cfg) { + + + return __reject() + + }, + + + listFiles: function listFiles(path) { + + + return __reject() + + }, + + + listDirs: function listDirs(path) { + + + return __reject() + + }, + + + setTitle: function setTitle(title) { + + + return __reject() + + }, + + + open: function open(uri) { + + + return __reject() + + }, + + + execute: function execute(command, args) { + + + return __reject() + + }, + + bridge: function bridge(command, payload) { + + + return __reject() + + }, + + loadAsset: function loadAsset(assetName, assetType) { + return this.promisified({ + cmd: 'loadAsset', + asset: assetName, + asset_type: assetType || 'unknown' + }) + } +}; + +// init tauri API +try { + window.tauri.invoke({ + cmd: 'init' + }) +} catch (e) { + window.addEventListener('DOMContentLoaded', function () { + window.tauri.invoke({ + cmd: 'init' + }) + }, true) +} + +document.addEventListener('error', function (e) { + var target = e.target + while (target != null) { + if (target.matches ? target.matches('img') : target.msMatchesSelector('img')) { + window.tauri.loadAsset(target.src, 'image') + .then(img => { + target.src = img + }) + break + } + target = target.parentElement + } +}, true) + +window.addEventListener('DOMContentLoaded', function () { + // open links with the Tauri API + document.querySelector('body').addEventListener('click', function (e) { + var target = e.target + while (target != null) { + if (target.matches ? target.matches('a') : target.msMatchesSelector('a')) { + window.tauri.open(target.href) + break + } + target = target.parentElement + } + }, true) +}, true)