├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── bookmarks.rs ├── contextual_identities.rs ├── downloads.rs ├── error.rs ├── event_listener.rs ├── history.rs ├── lib.rs ├── tabs │ ├── mod.rs │ ├── muted_info.rs │ ├── on_activated.rs │ ├── on_attached.rs │ ├── on_created.rs │ ├── on_detached.rs │ ├── on_highlighted.rs │ ├── on_moved.rs │ ├── on_removed.rs │ ├── on_replaced.rs │ ├── on_updated.rs │ ├── on_zoom_change.rs │ ├── query_details.rs │ ├── status.rs │ ├── tab.rs │ └── window_type.rs └── util.rs └── tests ├── contextual_identities.rs ├── tabs.rs └── util └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-extensions" 3 | version = "0.3.0" 4 | authors = [ 5 | "Markus Kohlhase ", 6 | "Roman Volosatovs ", 7 | ] 8 | edition = "2021" 9 | description = "This crate provides wrappers around WebExtensions API" 10 | repository = "https://github.com/rvolosatovs/web-extensions" 11 | license = "MIT" 12 | keywords = ["api", "serde", "wasm", "web", "webassembly"] 13 | categories = ["api-bindings", "wasm"] 14 | 15 | [lib] 16 | crate-type = ["cdylib", "rlib"] 17 | 18 | [dependencies] 19 | gloo-utils = "0.1.5" 20 | js-sys = "0.3.60" 21 | serde = { version = "1.0.147", features = ["derive"] } 22 | serde_derive = "1.0.147" 23 | serde_json = "1.0.87" 24 | thiserror = "1.0.37" 25 | wasm-bindgen = "0.2.83" 26 | wasm-bindgen-futures = "0.4.33" 27 | web-extensions-sys = "0.4.0" 28 | 29 | [dev-dependencies] 30 | wasm-bindgen-test = "0.3.33" 31 | 32 | [features] 33 | default = [] 34 | firefox = [] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Roman Volosatovs 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Extensions 2 | 3 | A Rust library that provides 4 | [WebExtension API](https://developer.chrome.com/docs/extensions/reference/) 5 | [WASM](https://en.wikipedia.org/wiki/WebAssembly) bindings. 6 | 7 | This crate expresses a high level wrapper. 8 | For a low level access there is the 9 | [`web-extensions-sys`](https://github.com/web-extensions-rs/web-extensions-sys) 10 | crate. 11 | 12 | ## Compatibility 13 | 14 | This library is currently only compatible with Chrome based browsers 15 | with [Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/). 16 | 17 | Once MV3 is supported by FireFox, we need to check how we can 18 | handle it. 19 | -------------------------------------------------------------------------------- /src/bookmarks.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper for the [`chrome.bookmarks` API](https://developer.chrome.com/docs/extensions/reference/bookmarks/). 2 | 3 | use crate::{util::*, Error}; 4 | use serde::{Deserialize, Serialize}; 5 | use web_extensions_sys as sys; 6 | 7 | /// 8 | pub async fn search(query: &Query<'_>) -> Result, Error> { 9 | let js_query = js_from_serde(query)?; 10 | let js_value = sys::chrome().bookmarks().search(&js_query).await; 11 | serde_from_js(js_value) 12 | } 13 | 14 | /// 15 | #[derive(Debug, Clone, Serialize)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct Query<'a> { 18 | /// A string of words and quoted phrases that are matched against bookmark URLs and titles. 19 | pub query: Option<&'a str>, 20 | 21 | /// The title of the bookmark; matches verbatim. 22 | pub title: Option<&'a str>, 23 | 24 | /// The URL of the bookmark; matches verbatim. Note that folders have no URL. 25 | pub url: Option<&'a str>, 26 | } 27 | 28 | impl<'a> From<&'a str> for Query<'a> { 29 | fn from(q: &'a str) -> Self { 30 | Self { 31 | query: Some(q), 32 | title: None, 33 | url: None, 34 | } 35 | } 36 | } 37 | 38 | /// 39 | /// 40 | /// A node (either a bookmark or a folder) in the bookmark tree. 41 | /// Child nodes are ordered within their parent folder. 42 | 43 | #[derive(Debug, Clone, Deserialize)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct BookmarkTreeNode { 46 | /// Unique identifier. 47 | pub id: String, 48 | 49 | /// An ordered list of children of this node. 50 | pub children: Option>, 51 | 52 | /// The 0-based position of this node within its parent folder. 53 | pub index: Option, 54 | 55 | /// A string which specifies the ID of the parent folder. This property is 56 | /// not present in the root node. 57 | pub parent_id: Option, 58 | 59 | /// Date and time of the creation of the bookmark. 60 | /// 61 | /// Unix time as milliseconds since the epoch. 62 | pub date_added: Option, 63 | 64 | /// When the contents of this folder last changed, in milliseconds since the epoch. 65 | pub date_group_modified: Option, 66 | 67 | /// The text displayed for the node in menus and lists of bookmarks. 68 | pub title: String, 69 | 70 | /// The URL for the bookmark. Empty if this node is a Folder. 71 | pub url: Option, 72 | } 73 | -------------------------------------------------------------------------------- /src/contextual_identities.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper for the [`browser.contextualIdentities` API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/contextualIdentities). 2 | 3 | use crate::{ 4 | util::{js_from_serde, object_from_js, serde_from_js_result}, 5 | Error, 6 | }; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | use web_extensions_sys::{browser, ContextualIdentities}; 10 | 11 | fn contextual_identities() -> ContextualIdentities { 12 | browser.contextual_identities() 13 | } 14 | 15 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 16 | pub enum Color { 17 | #[serde(rename(serialize = "blue", deserialize = "blue"))] 18 | Blue, 19 | #[serde(rename(serialize = "turquoise", deserialize = "turquoise"))] 20 | Turquoise, 21 | #[serde(rename(serialize = "green", deserialize = "green"))] 22 | Green, 23 | #[serde(rename(serialize = "yellow", deserialize = "yellow"))] 24 | Yellow, 25 | #[serde(rename(serialize = "orange", deserialize = "orange"))] 26 | Orange, 27 | #[serde(rename(serialize = "red", deserialize = "red"))] 28 | Red, 29 | #[serde(rename(serialize = "pink", deserialize = "pink"))] 30 | Pink, 31 | #[serde(rename(serialize = "purple", deserialize = "purple"))] 32 | Purple, 33 | #[serde(rename(serialize = "toolbar", deserialize = "toolbar"))] 34 | Toolbar, 35 | } 36 | 37 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 38 | pub enum Icon { 39 | #[serde(rename(serialize = "fingerprint", deserialize = "fingerprint"))] 40 | Fingerprint, 41 | #[serde(rename(serialize = "briefcase", deserialize = "briefcase"))] 42 | Briefcase, 43 | #[serde(rename(serialize = "dollar", deserialize = "dollar"))] 44 | Dollar, 45 | #[serde(rename(serialize = "cart", deserialize = "cart"))] 46 | Cart, 47 | #[serde(rename(serialize = "circle", deserialize = "circle"))] 48 | Circle, 49 | #[serde(rename(serialize = "gift", deserialize = "gift"))] 50 | Gift, 51 | #[serde(rename(serialize = "vacation", deserialize = "vacation"))] 52 | Vacation, 53 | #[serde(rename(serialize = "food", deserialize = "food"))] 54 | Food, 55 | #[serde(rename(serialize = "fruit", deserialize = "fruit"))] 56 | Fruit, 57 | #[serde(rename(serialize = "pet", deserialize = "pet"))] 58 | Pet, 59 | #[serde(rename(serialize = "tree", deserialize = "tree"))] 60 | Tree, 61 | #[serde(rename(serialize = "chill", deserialize = "chill"))] 62 | Chill, 63 | #[serde(rename(serialize = "fence", deserialize = "fence"))] 64 | Fence, 65 | } 66 | 67 | #[derive(Debug, PartialEq, Eq, Deserialize)] 68 | #[serde(rename_all = "camelCase")] 69 | pub struct ContextualIdentity { 70 | pub cookie_store_id: String, 71 | pub color: Color, 72 | pub color_code: String, 73 | pub icon: Icon, 74 | pub icon_url: String, 75 | pub name: String, 76 | } 77 | 78 | #[derive(Serialize)] 79 | pub struct CreateDetails<'a> { 80 | pub name: &'a str, 81 | pub color: Color, 82 | pub icon: Icon, 83 | } 84 | 85 | pub async fn create(details: &CreateDetails<'_>) -> Result { 86 | serde_from_js_result( 87 | contextual_identities() 88 | .create(object_from_js(&js_from_serde(details)?)?) 89 | .await, 90 | ) 91 | } 92 | 93 | pub async fn get(cookie_store_id: &str) -> Result { 94 | serde_from_js_result(contextual_identities().get(cookie_store_id).await) 95 | } 96 | 97 | #[derive(Serialize)] 98 | pub struct QueryDetails<'a> { 99 | pub name: Option<&'a str>, 100 | } 101 | 102 | pub async fn query(details: &QueryDetails<'_>) -> Result, Error> { 103 | serde_from_js_result( 104 | contextual_identities() 105 | .query(object_from_js(&js_from_serde(details)?)?) 106 | .await, 107 | ) 108 | } 109 | 110 | pub async fn remove(cookie_store_id: &str) -> Result { 111 | serde_from_js_result(contextual_identities().remove(cookie_store_id).await) 112 | } 113 | 114 | #[derive(Serialize)] 115 | pub struct UpdateDetails<'a> { 116 | pub name: Option<&'a str>, 117 | pub color: Option, 118 | pub icon: Option, 119 | } 120 | 121 | pub async fn update( 122 | cookie_store_id: &str, 123 | details: &UpdateDetails<'_>, 124 | ) -> Result { 125 | serde_from_js_result( 126 | contextual_identities() 127 | .update(cookie_store_id, object_from_js(&js_from_serde(details)?)?) 128 | .await, 129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /src/downloads.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper for the [`chrome.downloads` API](https://developer.chrome.com/docs/extensions/reference/downloads/). 2 | 3 | use crate::{util::*, Error}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::path::PathBuf; 6 | use web_extensions_sys as sys; 7 | 8 | /// 9 | pub async fn search(query: &Query<'_>) -> Result, Error> { 10 | let js_query = js_from_serde(query)?; 11 | let js_value = sys::chrome().downloads().search(&js_query).await?; 12 | serde_from_js(js_value) 13 | } 14 | 15 | /// 16 | #[derive(Debug, Clone, Serialize)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct Query<'a> { 19 | pub query: Option>, 20 | pub start_time: Option<&'a str>, 21 | } 22 | 23 | impl<'a> From<&'a str> for Query<'a> { 24 | fn from(q: &'a str) -> Self { 25 | Self { 26 | query: Some(vec![q]), 27 | start_time: None, 28 | } 29 | } 30 | } 31 | 32 | /// 33 | #[derive(Debug, Clone, Deserialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct DownloadItem { 36 | pub filename: PathBuf, 37 | pub mime: String, 38 | pub start_time: String, 39 | pub url: String, 40 | } 41 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use gloo_utils::format::JsValueSerdeExt; 2 | use thiserror::Error; 3 | use wasm_bindgen::{convert::FromWasmAbi, describe::WasmDescribe, prelude::*}; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | #[error("JavaScript error: {0:?}")] 8 | Js(JsValue), 9 | #[error(transparent)] 10 | JsonDeserialization(serde_json::Error), 11 | #[error(transparent)] 12 | JsonSerialization(serde_json::Error), 13 | #[error("Unable to convert JS value to an JS object")] 14 | ObjectConversion, 15 | } 16 | 17 | impl From for Error { 18 | fn from(err: JsValue) -> Self { 19 | Self::Js(err) 20 | } 21 | } 22 | 23 | #[derive(Debug)] 24 | pub enum FromWasmAbiResult { 25 | /// Contains the success value 26 | Ok(T), 27 | 28 | /// Contains the error value 29 | Err(E), 30 | } 31 | 32 | impl From> for Result { 33 | fn from(result: FromWasmAbiResult) -> Self { 34 | match result { 35 | FromWasmAbiResult::Ok(v) => Ok(v), 36 | FromWasmAbiResult::Err(e) => Err(e), 37 | } 38 | } 39 | } 40 | 41 | impl From> for FromWasmAbiResult { 42 | fn from(result: Result) -> Self { 43 | match result { 44 | Ok(v) => Self::Ok(v), 45 | Err(e) => Self::Err(e), 46 | } 47 | } 48 | } 49 | 50 | pub type SerdeFromWasmAbiResult = FromWasmAbiResult; 51 | 52 | impl WasmDescribe for SerdeFromWasmAbiResult { 53 | #[inline] 54 | fn describe() { 55 | JsValue::describe() 56 | } 57 | } 58 | impl serde::Deserialize<'a>> FromWasmAbi for SerdeFromWasmAbiResult { 59 | type Abi = u32; 60 | 61 | #[inline] 62 | unsafe fn from_abi(js: u32) -> Self { 63 | JsValue::from_abi(js).into_serde().into() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/event_listener.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{closure::WasmClosure, prelude::*, JsCast}; 2 | use web_extensions_sys as sys; 3 | 4 | // Adapted from https://github.com/rustwasm/gloo/blob/2c9e776701ecb90c53e62dec1abd19c2b70e47c7/crates/events/src/lib.rs#L232-L582 5 | #[must_use = "event listener will never be called after being dropped"] 6 | pub struct EventListener<'a, F: ?Sized> { 7 | target: &'a sys::EventTarget, 8 | callback: Option>, 9 | } 10 | 11 | impl<'a, F> EventListener<'a, F> 12 | where 13 | F: ?Sized + WasmClosure, 14 | { 15 | #[inline] 16 | pub(crate) fn raw_new(target: &'a sys::EventTarget, callback: Closure) -> Self { 17 | target.add_listener(callback.as_ref().unchecked_ref()); 18 | Self { 19 | target, 20 | callback: Some(callback), 21 | } 22 | } 23 | 24 | /// Keeps the `EventListener` alive forever, so it will never be dropped. 25 | /// 26 | /// This should only be used when you want the `EventListener` to last forever, otherwise it will leak memory! 27 | #[inline] 28 | pub fn forget(mut self) { 29 | // take() is necessary because of Rust's restrictions about Drop 30 | if let Some(callback) = self.callback.take() { 31 | callback.forget(); 32 | } 33 | } 34 | } 35 | 36 | impl Drop for EventListener<'_, F> { 37 | #[inline] 38 | fn drop(&mut self) { 39 | if let Some(callback) = &self.callback { 40 | self.target 41 | .remove_listener(callback.as_ref().unchecked_ref()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/history.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper for the [`chrome.history` API](https://developer.chrome.com/docs/extensions/reference/history/). 2 | 3 | use crate::{util::*, Error}; 4 | use serde::{Deserialize, Serialize}; 5 | use web_extensions_sys as sys; 6 | 7 | /// 8 | pub async fn search(query: &Query<'_>) -> Result, Error> { 9 | let js_query = js_from_serde(query)?; 10 | let js_value = sys::chrome() 11 | .history() 12 | .search(object_from_js(&js_query)?) 13 | .await; 14 | serde_from_js(js_value) 15 | } 16 | 17 | /// 18 | #[derive(Default, Debug, Clone, Serialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct Query<'a> { 21 | /// A free-text query to the history service. 22 | /// 23 | /// Leave empty to retrieve all pages. 24 | pub text: &'a str, 25 | 26 | /// Limit results to those visited before this date, 27 | /// represented in milliseconds since the epoch. 28 | pub end_time: Option, 29 | 30 | /// The maximum number of results to retrieve. 31 | /// 32 | /// Defaults to 100. 33 | pub max_results: Option, 34 | 35 | /// Limit results to those visited after this date, 36 | /// represented in milliseconds since the epoch. 37 | /// 38 | /// If not specified, this defaults to 24 hours in the past. 39 | pub start_time: Option, 40 | } 41 | 42 | impl<'a> From<&'a str> for Query<'a> { 43 | fn from(q: &'a str) -> Self { 44 | Self { 45 | text: q, 46 | ..Default::default() 47 | } 48 | } 49 | } 50 | 51 | /// 52 | /// 53 | /// An object encapsulating one result of a history query. 54 | 55 | #[derive(Debug, Clone, Deserialize)] 56 | #[serde(rename_all = "camelCase")] 57 | pub struct HistoryItem { 58 | /// Unique identifier. 59 | pub id: String, 60 | 61 | /// When this page was last loaded, represented in milliseconds since the epoch. 62 | // NOTE: chrome returns floating point values, so i64 does not work here. 63 | pub last_visit_time: Option, 64 | 65 | /// The title of the page when it was last loaded. 66 | pub title: Option, 67 | 68 | /// The number of times the user has navigated to this page by typing in the address. 69 | pub typed_count: Option, 70 | 71 | /// The URL of the page. 72 | pub url: Option, 73 | 74 | /// The number of times the user has visited the page. 75 | pub visit_count: Option, 76 | } 77 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod event_listener; 3 | mod util; 4 | 5 | pub use crate::error::*; 6 | 7 | pub mod bookmarks; 8 | pub mod downloads; 9 | pub mod history; 10 | pub mod tabs; 11 | 12 | #[cfg(feature = "firefox")] 13 | pub mod contextual_identities; 14 | -------------------------------------------------------------------------------- /src/tabs/mod.rs: -------------------------------------------------------------------------------- 1 | //! Wrapper for the [`chrome.tabs` API](https://developer.chrome.com/docs/extensions/reference/tabs/). 2 | 3 | pub(crate) mod prelude { 4 | pub(crate) use crate::util::{js_from_serde, object_from_js, serde_from_js_result}; 5 | pub use crate::{event_listener::EventListener, tabs::TabId, Error}; 6 | pub use serde::{Deserialize, Serialize}; 7 | pub use wasm_bindgen::closure::Closure; 8 | pub use web_extensions_sys as sys; 9 | 10 | pub fn tabs() -> sys::Tabs { 11 | // Currently we assume a chrome browser and Manifest V3. 12 | // 13 | // Once MV3 is supported by FireFox, we need to check if we can use the same namespace, 14 | // a shim or our own implementation. 15 | sys::chrome().tabs() 16 | } 17 | } 18 | 19 | use self::prelude::*; 20 | 21 | /// The ID of the tab. 22 | /// 23 | /// Tab IDs are unique within a browser session. 24 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 25 | pub struct TabId(i32); 26 | 27 | impl From for TabId { 28 | fn from(id: i32) -> Self { 29 | Self(id) 30 | } 31 | } 32 | 33 | mod on_activated; 34 | mod on_attached; 35 | mod on_created; 36 | mod on_detached; 37 | mod on_highlighted; 38 | mod on_moved; 39 | mod on_removed; 40 | mod on_replaced; 41 | mod on_updated; 42 | mod on_zoom_change; 43 | 44 | mod muted_info; 45 | mod query_details; 46 | mod status; 47 | mod tab; 48 | mod window_type; 49 | 50 | pub use self::{ 51 | muted_info::*, on_activated::*, on_attached::*, on_created::*, on_detached::*, 52 | on_highlighted::*, on_moved::*, on_removed::*, on_replaced::*, on_updated::*, 53 | on_zoom_change::*, query_details::*, status::*, tab::*, window_type::*, 54 | }; 55 | 56 | /// 57 | pub async fn get(tab_id: TabId) -> Result { 58 | let result = tabs().get(tab_id.0).await; 59 | serde_from_js_result(result) 60 | } 61 | 62 | /// 63 | pub async fn query(details: &QueryDetails<'_>) -> Result, Error> { 64 | let js_details = js_from_serde(details)?; 65 | let result = tabs().query(object_from_js(&js_details)?).await; 66 | serde_from_js_result(result) 67 | } 68 | 69 | /// 70 | pub async fn send_message(tab_id: TabId, message: &T) -> Result<(), Error> 71 | where 72 | T: Serialize, 73 | { 74 | let js_message = js_from_serde(message)?; 75 | let options = None; 76 | tabs() 77 | .send_message(tab_id.0, &js_message, options) 78 | .await 79 | .map(|_| ())?; 80 | Ok(()) 81 | } 82 | 83 | /// 84 | pub async fn create(props: CreateProperties<'_>) -> Result { 85 | let js_props = js_from_serde(&props)?; 86 | let result = tabs().create(object_from_js(&js_props)?).await; 87 | serde_from_js_result(result) 88 | } 89 | 90 | /// Information necessary to open a new tab. 91 | #[derive(Debug, Serialize, Deserialize)] 92 | pub struct CreateProperties<'a> { 93 | pub active: bool, 94 | pub url: &'a str, 95 | } 96 | -------------------------------------------------------------------------------- /src/tabs/muted_info.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | #[derive(Debug, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct MutedInfo { 7 | pub muted: bool, 8 | pub extension_id: Option, 9 | pub reason: Option, 10 | } 11 | 12 | impl From for MutedInfo { 13 | fn from(info: sys::TabMutedInfo) -> Self { 14 | Self { 15 | muted: info.muted(), 16 | extension_id: info.extension_id(), 17 | reason: info.reason(), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/tabs/on_activated.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | pub fn on_activated() -> OnActivated { 5 | OnActivated(tabs().on_activated()) 6 | } 7 | 8 | /// 9 | pub struct OnActivated(sys::EventTarget); 10 | 11 | /// 12 | pub struct OnActivatedEventListener<'a>(EventListener<'a, dyn FnMut(sys::TabActiveInfo)>); 13 | 14 | impl OnActivatedEventListener<'_> { 15 | pub fn forget(self) { 16 | self.0.forget() 17 | } 18 | } 19 | 20 | impl OnActivated { 21 | pub fn add_listener(&self, mut listener: L) -> OnActivatedEventListener 22 | where 23 | L: FnMut(ActiveInfo) + 'static, 24 | { 25 | let listener = 26 | Closure::new(move |info: sys::TabActiveInfo| listener(ActiveInfo::from(info))); 27 | OnActivatedEventListener(EventListener::raw_new(&self.0, listener)) 28 | } 29 | } 30 | 31 | /// 32 | #[derive(Debug, Deserialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct ActiveInfo { 35 | pub tab_id: TabId, 36 | pub window_id: i32, 37 | } 38 | 39 | impl From for ActiveInfo { 40 | fn from(info: sys::TabActiveInfo) -> Self { 41 | let tab_id = TabId::from(info.tab_id()); 42 | ActiveInfo { 43 | tab_id, 44 | window_id: info.window_id(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/tabs/on_attached.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | pub fn on_attached() -> OnAttached { 5 | OnAttached(tabs().on_attached()) 6 | } 7 | 8 | /// 9 | pub struct OnAttached(sys::EventTarget); 10 | 11 | pub struct OnAttachedEventListener<'a>(EventListener<'a, dyn FnMut(i32, sys::TabAttachInfo)>); 12 | 13 | impl OnAttachedEventListener<'_> { 14 | pub fn forget(self) { 15 | self.0.forget() 16 | } 17 | } 18 | 19 | impl OnAttached { 20 | pub fn add_listener(&self, mut listener: L) -> OnAttachedEventListener 21 | where 22 | L: FnMut(TabId, AttachInfo) + 'static, 23 | { 24 | let listener = Closure::new(move |tab_id: i32, info: sys::TabAttachInfo| { 25 | listener(TabId::from(tab_id), AttachInfo::from(info)) 26 | }); 27 | OnAttachedEventListener(EventListener::raw_new(&self.0, listener)) 28 | } 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct AttachInfo { 34 | pub new_window_id: i32, 35 | pub new_position: u32, 36 | } 37 | 38 | impl From for AttachInfo { 39 | fn from(info: sys::TabAttachInfo) -> Self { 40 | Self { 41 | new_position: info.new_position(), 42 | new_window_id: info.new_window_id(), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/tabs/on_created.rs: -------------------------------------------------------------------------------- 1 | use super::{prelude::*, Tab}; 2 | 3 | /// 4 | pub fn on_created() -> OnCreated { 5 | OnCreated(tabs().on_created()) 6 | } 7 | 8 | /// 9 | pub struct OnCreated(sys::EventTarget); 10 | 11 | pub struct OnCreatedEventListener<'a>(EventListener<'a, dyn FnMut(sys::Tab)>); 12 | 13 | impl OnCreatedEventListener<'_> { 14 | pub fn forget(self) { 15 | self.0.forget() 16 | } 17 | } 18 | 19 | impl OnCreated { 20 | pub fn add_listener(&self, mut listener: L) -> OnCreatedEventListener 21 | where 22 | L: FnMut(Tab) + 'static, 23 | { 24 | let listener = Closure::new(move |tab: sys::Tab| listener(Tab::from(tab))); 25 | OnCreatedEventListener(EventListener::raw_new(&self.0, listener)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/tabs/on_detached.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | pub fn on_detached() -> OnDetached { 5 | OnDetached(tabs().on_detached()) 6 | } 7 | 8 | /// 9 | pub struct OnDetached(sys::EventTarget); 10 | 11 | pub struct OnDetachedEventListener<'a>(EventListener<'a, dyn FnMut(i32, sys::TabDetachInfo)>); 12 | 13 | impl OnDetachedEventListener<'_> { 14 | pub fn forget(self) { 15 | self.0.forget() 16 | } 17 | } 18 | 19 | impl OnDetached { 20 | pub fn add_listener(&self, mut listener: L) -> OnDetachedEventListener 21 | where 22 | L: FnMut(i32, DetachInfo) + 'static, 23 | { 24 | let listener = Closure::new(move |tab_id: i32, info: sys::TabDetachInfo| { 25 | listener(tab_id, DetachInfo::from(info)) 26 | }); 27 | OnDetachedEventListener(EventListener::raw_new(&self.0, listener)) 28 | } 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct DetachInfo { 34 | pub old_window_id: i32, 35 | pub old_position: u32, 36 | } 37 | 38 | impl From for DetachInfo { 39 | fn from(info: sys::TabDetachInfo) -> Self { 40 | Self { 41 | old_position: info.old_position(), 42 | old_window_id: info.old_window_id(), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/tabs/on_highlighted.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use gloo_utils::format::JsValueSerdeExt; 3 | 4 | /// 5 | pub fn on_highlighted() -> OnHighlighted { 6 | OnHighlighted(tabs().on_highlighted()) 7 | } 8 | 9 | /// 10 | pub struct OnHighlighted(sys::EventTarget); 11 | 12 | pub struct OnHighlightedEventListener<'a>(EventListener<'a, dyn FnMut(sys::TabHighlightInfo)>); 13 | 14 | impl OnHighlightedEventListener<'_> { 15 | pub fn forget(self) { 16 | self.0.forget() 17 | } 18 | } 19 | 20 | impl OnHighlighted { 21 | pub fn add_listener(&self, mut listener: L) -> OnHighlightedEventListener 22 | where 23 | L: FnMut(HighlightInfo) + 'static, 24 | { 25 | let listener = 26 | Closure::new(move |info: sys::TabHighlightInfo| listener(HighlightInfo::from(info))); 27 | OnHighlightedEventListener(EventListener::raw_new(&self.0, listener)) 28 | } 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct HighlightInfo { 34 | pub window_id: i32, 35 | pub tab_ids: Vec, 36 | } 37 | 38 | impl From for HighlightInfo { 39 | fn from(info: sys::TabHighlightInfo) -> Self { 40 | let tab_ids = info.tab_ids().into_serde().expect("Tab IDs"); 41 | Self { 42 | tab_ids, 43 | window_id: info.window_id(), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/tabs/on_moved.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | pub fn on_moved() -> OnMoved { 5 | OnMoved(tabs().on_moved()) 6 | } 7 | 8 | /// 9 | pub struct OnMoved(sys::EventTarget); 10 | 11 | pub struct OnMovedEventListener<'a>(EventListener<'a, dyn FnMut(i32, sys::TabMoveInfo)>); 12 | 13 | impl OnMovedEventListener<'_> { 14 | pub fn forget(self) { 15 | self.0.forget() 16 | } 17 | } 18 | 19 | impl OnMoved { 20 | pub fn add_listener(&self, mut listener: L) -> OnMovedEventListener 21 | where 22 | L: FnMut(TabId, MoveInfo) + 'static, 23 | { 24 | let listener = Closure::new(move |tab_id: i32, tab: sys::TabMoveInfo| { 25 | listener(TabId::from(tab_id), MoveInfo::from(tab)) 26 | }); 27 | OnMovedEventListener(EventListener::raw_new(&self.0, listener)) 28 | } 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct MoveInfo { 34 | pub window_id: i32, 35 | pub from_index: u32, 36 | pub to_index: u32, 37 | } 38 | 39 | impl From for MoveInfo { 40 | fn from(info: sys::TabMoveInfo) -> Self { 41 | Self { 42 | from_index: info.from_index(), 43 | to_index: info.to_index(), 44 | window_id: info.window_id(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/tabs/on_removed.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | pub fn on_removed() -> OnRemoved { 5 | OnRemoved(tabs().on_removed()) 6 | } 7 | 8 | /// 9 | pub struct OnRemoved(sys::EventTarget); 10 | 11 | pub struct OnRemovedEventListener<'a>(EventListener<'a, dyn FnMut(i32, sys::TabRemoveInfo)>); 12 | 13 | impl OnRemovedEventListener<'_> { 14 | pub fn forget(self) { 15 | self.0.forget() 16 | } 17 | } 18 | 19 | impl OnRemoved { 20 | pub fn add_listener(&self, mut listener: L) -> OnRemovedEventListener 21 | where 22 | L: FnMut(TabId, RemoveInfo) + 'static, 23 | { 24 | let listener = Closure::new(move |tab_id: i32, info: sys::TabRemoveInfo| { 25 | listener(TabId::from(tab_id), RemoveInfo::from(info)) 26 | }); 27 | OnRemovedEventListener(EventListener::raw_new(&self.0, listener)) 28 | } 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct RemoveInfo { 34 | pub window_id: i32, 35 | pub is_window_closing: bool, 36 | } 37 | 38 | impl From for RemoveInfo { 39 | fn from(info: sys::TabRemoveInfo) -> Self { 40 | Self { 41 | window_id: info.window_id(), 42 | is_window_closing: info.is_window_closing(), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/tabs/on_replaced.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | pub fn on_replaced() -> OnReplaced { 5 | OnReplaced(tabs().on_replaced()) 6 | } 7 | 8 | /// 9 | pub struct OnReplaced(sys::EventTarget); 10 | 11 | pub struct OnReplacedEventListener<'a>(EventListener<'a, dyn FnMut(i32, i32)>); 12 | 13 | impl OnReplacedEventListener<'_> { 14 | pub fn forget(self) { 15 | self.0.forget() 16 | } 17 | } 18 | 19 | impl OnReplaced { 20 | pub fn add_listener(&self, mut listener: L) -> OnReplacedEventListener 21 | where 22 | L: FnMut(ReplaceInfo) + 'static, 23 | { 24 | let listener = Closure::new(move |added_tab_id: i32, removed_tab_id: i32| { 25 | let replace_info = ReplaceInfo { 26 | added: TabId::from(added_tab_id), 27 | removed: TabId::from(removed_tab_id), 28 | }; 29 | listener(replace_info) 30 | }); 31 | OnReplacedEventListener(EventListener::raw_new(&self.0, listener)) 32 | } 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct ReplaceInfo { 37 | pub added: TabId, 38 | pub removed: TabId, 39 | } 40 | -------------------------------------------------------------------------------- /src/tabs/on_updated.rs: -------------------------------------------------------------------------------- 1 | use super::{prelude::*, MutedInfo, Status, Tab}; 2 | 3 | /// 4 | pub fn on_updated() -> OnUpdated { 5 | OnUpdated(tabs().on_updated()) 6 | } 7 | 8 | /// 9 | pub struct OnUpdated(sys::EventTarget); 10 | 11 | /// 12 | pub struct OnUpdatedEventListener<'a>( 13 | EventListener<'a, dyn FnMut(i32, sys::TabChangeInfo, sys::Tab)>, 14 | ); 15 | 16 | impl OnUpdatedEventListener<'_> { 17 | pub fn forget(self) { 18 | self.0.forget() 19 | } 20 | } 21 | 22 | impl OnUpdated { 23 | pub fn add_listener(&self, mut listener: L) -> OnUpdatedEventListener 24 | where 25 | L: FnMut(TabId, ChangeInfo, Tab) + 'static, 26 | { 27 | let listener = Closure::new( 28 | move |tab_id: i32, info: sys::TabChangeInfo, tab: sys::Tab| { 29 | listener(TabId::from(tab_id), ChangeInfo::from(info), Tab::from(tab)) 30 | }, 31 | ); 32 | OnUpdatedEventListener(EventListener::raw_new(&self.0, listener)) 33 | } 34 | } 35 | 36 | /// 37 | #[derive(Debug, Deserialize)] 38 | #[serde(rename_all = "camelCase")] 39 | pub struct ChangeInfo { 40 | /// The tab's new audible state. 41 | pub audible: Option, 42 | 43 | /// The tab's new auto-discardable state. 44 | pub auto_discardable: Option, 45 | 46 | /// The tab's new discarded state. 47 | pub discarded: Option, 48 | 49 | /// The tab's new favicon URL. 50 | pub fav_icon_url: Option, 51 | 52 | /// The tab's new group. 53 | pub group_id: Option, 54 | 55 | /// The tab's new muted state and the reason for the change. 56 | pub muted_info: Option, 57 | 58 | /// The tab's new pinned state. 59 | pub pinned: Option, 60 | 61 | /// The tab's loading status. 62 | pub status: Option, 63 | 64 | /// The tab's new title. 65 | pub title: Option, 66 | 67 | /// The tab's URL if it has changed. 68 | pub url: Option, 69 | } 70 | 71 | impl From for ChangeInfo { 72 | fn from(info: sys::TabChangeInfo) -> Self { 73 | let status = info.status().map(|s| Status::try_from(s).expect("status")); 74 | let muted_info = info.muted_info().map(MutedInfo::from); 75 | Self { 76 | status, 77 | muted_info, 78 | audible: info.audible(), 79 | auto_discardable: info.auto_discardable(), 80 | discarded: info.discarded(), 81 | fav_icon_url: info.fav_icon_url(), 82 | group_id: info.group_id(), 83 | pinned: info.pinned(), 84 | title: info.title(), 85 | url: info.url(), 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/tabs/on_zoom_change.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | pub fn on_zoom_change() -> OnZoomChange { 5 | OnZoomChange(tabs().on_zoom_change()) 6 | } 7 | 8 | /// 9 | pub struct OnZoomChange(sys::EventTarget); 10 | 11 | pub struct OnZoomChangeEventListener<'a>(EventListener<'a, dyn FnMut(sys::TabZoomChangeInfo)>); 12 | 13 | impl OnZoomChangeEventListener<'_> { 14 | pub fn forget(self) { 15 | self.0.forget() 16 | } 17 | } 18 | 19 | impl OnZoomChange { 20 | pub fn add_listener(&self, mut listener: L) -> OnZoomChangeEventListener 21 | where 22 | L: FnMut(ZoomChangeInfo) + 'static, 23 | { 24 | let listener = 25 | Closure::new(move |info: sys::TabZoomChangeInfo| listener(ZoomChangeInfo::from(info))); 26 | OnZoomChangeEventListener(EventListener::raw_new(&self.0, listener)) 27 | } 28 | } 29 | 30 | #[derive(Debug, Deserialize)] 31 | #[serde(rename_all = "camelCase")] 32 | pub struct ZoomChangeInfo { 33 | // TODO: Add more fields from https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onZoomChange#ZoomChangeInfo 34 | pub new_zoom_factor: f64, 35 | pub old_zoom_factor: f64, 36 | pub tab_id: i32, 37 | } 38 | 39 | impl From for ZoomChangeInfo { 40 | fn from(info: sys::TabZoomChangeInfo) -> Self { 41 | Self { 42 | new_zoom_factor: info.new_zoom_factor(), 43 | old_zoom_factor: info.old_zoom_factor(), 44 | tab_id: info.tab_id(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/tabs/query_details.rs: -------------------------------------------------------------------------------- 1 | use super::{prelude::*, Status, WindowType}; 2 | 3 | /// 4 | #[derive(Debug, Serialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct QueryDetails<'a> { 7 | pub active: Option, 8 | pub audible: Option, 9 | pub auto_discardable: Option, 10 | pub cookie_store_id: Option<&'a str>, 11 | pub current_window: Option, 12 | pub discarded: Option, 13 | pub hidden: Option, 14 | pub highlighted: Option, 15 | pub index: Option, 16 | pub muted: Option, 17 | pub last_focused_window: Option, 18 | pub pinned: Option, 19 | pub status: Option, 20 | pub title: Option<&'a str>, 21 | pub url: Option<&'a str>, 22 | pub window_id: i32, 23 | pub window_type: Option, 24 | } 25 | -------------------------------------------------------------------------------- /src/tabs/status.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | use thiserror::Error; 3 | 4 | /// 5 | #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] 6 | pub enum Status { 7 | #[serde(rename(serialize = "unloaded", deserialize = "unloaded"))] 8 | Unloaded, 9 | #[serde(rename(serialize = "loading", deserialize = "loading"))] 10 | Loading, 11 | #[serde(rename(serialize = "complete", deserialize = "complete"))] 12 | Complete, 13 | } 14 | 15 | #[derive(Debug, Error)] 16 | #[error("Invalid status ('{0}'), expected 'unloaded', 'loading' or 'complete'")] 17 | pub struct InvalidStatusError(String); 18 | 19 | impl TryFrom for Status { 20 | type Error = InvalidStatusError; 21 | fn try_from(s: String) -> Result { 22 | match &*s { 23 | "unloaded" => Ok(Status::Unloaded), 24 | "loading" => Ok(Status::Loading), 25 | "complete" => Ok(Status::Complete), 26 | _ => Err(InvalidStatusError(s)), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/tabs/tab.rs: -------------------------------------------------------------------------------- 1 | use super::{prelude::*, Status}; 2 | 3 | /// 4 | #[derive(Debug, PartialEq, Eq, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Tab { 7 | pub active: bool, 8 | pub audible: Option, 9 | pub auto_discardable: bool, 10 | pub discarded: bool, 11 | pub fav_icon_url: Option, 12 | pub group_id: i32, 13 | pub height: Option, 14 | pub highlighted: bool, 15 | pub id: Option, 16 | pub incognito: bool, 17 | pub index: u32, 18 | // TODO: muted_info 19 | pub opener_tab_id: Option, 20 | pub pending_url: Option, 21 | pub pinned: bool, 22 | pub session_id: Option, 23 | pub status: Option, 24 | pub title: Option, 25 | pub url: Option, 26 | pub width: Option, 27 | pub window_id: i32, 28 | } 29 | 30 | impl From for Tab { 31 | fn from(info: sys::Tab) -> Self { 32 | let status = info.status().map(|s| Status::try_from(s).expect("status")); 33 | let id = info.id().map(TabId::from); 34 | let opener_tab_id = info.opener_tab_id().map(TabId::from); 35 | Self { 36 | id, 37 | opener_tab_id, 38 | status, 39 | active: info.active(), 40 | audible: info.audible(), 41 | auto_discardable: info.auto_discardable(), 42 | discarded: info.discarded(), 43 | fav_icon_url: info.fav_icon_url(), 44 | group_id: info.group_id(), 45 | height: info.height(), 46 | highlighted: info.highlighted(), 47 | incognito: info.incognito(), 48 | index: info.index(), 49 | pending_url: info.pending_url(), 50 | pinned: info.pinned(), 51 | session_id: info.session_id(), 52 | title: info.title(), 53 | url: info.url(), 54 | width: info.width(), 55 | window_id: info.window_id(), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/tabs/window_type.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::*; 2 | 3 | /// 4 | #[derive(Debug, PartialEq, Eq, Serialize)] 5 | pub enum WindowType { 6 | #[serde(rename(serialize = "normal"))] 7 | Normal, 8 | #[serde(rename(serialize = "popup"))] 9 | Popup, 10 | #[serde(rename(serialize = "panel"))] 11 | Panel, 12 | #[serde(rename(serialize = "devtools"))] 13 | Devtools, 14 | } 15 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use gloo_utils::format::JsValueSerdeExt; 3 | use js_sys::Object; 4 | use wasm_bindgen::prelude::*; 5 | 6 | pub(crate) fn js_from_serde(v: &T) -> Result { 7 | JsValue::from_serde(v).map_err(Error::JsonSerialization) 8 | } 9 | 10 | pub(crate) fn object_from_js(v: &JsValue) -> Result<&Object, Error> { 11 | Object::try_from(v).ok_or(Error::ObjectConversion) 12 | } 13 | 14 | pub(crate) fn serde_from_js_result(v: Result) -> Result 15 | where 16 | T: for<'a> serde::Deserialize<'a>, 17 | { 18 | v?.into_serde().map_err(Error::JsonDeserialization) 19 | } 20 | 21 | pub(crate) fn serde_from_js(v: JsValue) -> Result 22 | where 23 | T: for<'a> serde::Deserialize<'a>, 24 | { 25 | v.into_serde().map_err(Error::JsonDeserialization) 26 | } 27 | -------------------------------------------------------------------------------- /tests/contextual_identities.rs: -------------------------------------------------------------------------------- 1 | use web_extensions::contextual_identities::*; 2 | 3 | mod util; 4 | use util::*; 5 | 6 | #[test] 7 | fn color_serde() { 8 | assert_json_serde_test_cases(&[ 9 | JSONSerdeTestCase { 10 | value: Color::Blue, 11 | json: r#""blue""#, 12 | }, 13 | JSONSerdeTestCase { 14 | value: Color::Turquoise, 15 | json: r#""turquoise""#, 16 | }, 17 | JSONSerdeTestCase { 18 | value: Color::Green, 19 | json: r#""green""#, 20 | }, 21 | JSONSerdeTestCase { 22 | value: Color::Yellow, 23 | json: r#""yellow""#, 24 | }, 25 | JSONSerdeTestCase { 26 | value: Color::Orange, 27 | json: r#""orange""#, 28 | }, 29 | JSONSerdeTestCase { 30 | value: Color::Red, 31 | json: r#""red""#, 32 | }, 33 | JSONSerdeTestCase { 34 | value: Color::Pink, 35 | json: r#""pink""#, 36 | }, 37 | JSONSerdeTestCase { 38 | value: Color::Purple, 39 | json: r#""purple""#, 40 | }, 41 | JSONSerdeTestCase { 42 | value: Color::Toolbar, 43 | json: r#""toolbar""#, 44 | }, 45 | ]) 46 | } 47 | 48 | #[test] 49 | fn icon_serde() { 50 | assert_json_serde_test_cases(&[ 51 | JSONSerdeTestCase { 52 | value: Icon::Fingerprint, 53 | json: r#""fingerprint""#, 54 | }, 55 | JSONSerdeTestCase { 56 | value: Icon::Briefcase, 57 | json: r#""briefcase""#, 58 | }, 59 | JSONSerdeTestCase { 60 | value: Icon::Dollar, 61 | json: r#""dollar""#, 62 | }, 63 | JSONSerdeTestCase { 64 | value: Icon::Cart, 65 | json: r#""cart""#, 66 | }, 67 | JSONSerdeTestCase { 68 | value: Icon::Circle, 69 | json: r#""circle""#, 70 | }, 71 | JSONSerdeTestCase { 72 | value: Icon::Gift, 73 | json: r#""gift""#, 74 | }, 75 | JSONSerdeTestCase { 76 | value: Icon::Vacation, 77 | json: r#""vacation""#, 78 | }, 79 | JSONSerdeTestCase { 80 | value: Icon::Food, 81 | json: r#""food""#, 82 | }, 83 | JSONSerdeTestCase { 84 | value: Icon::Fruit, 85 | json: r#""fruit""#, 86 | }, 87 | JSONSerdeTestCase { 88 | value: Icon::Pet, 89 | json: r#""pet""#, 90 | }, 91 | JSONSerdeTestCase { 92 | value: Icon::Tree, 93 | json: r#""tree""#, 94 | }, 95 | JSONSerdeTestCase { 96 | value: Icon::Chill, 97 | json: r#""chill""#, 98 | }, 99 | JSONSerdeTestCase { 100 | value: Icon::Fence, 101 | json: r#""fence""#, 102 | }, 103 | ]) 104 | } 105 | -------------------------------------------------------------------------------- /tests/tabs.rs: -------------------------------------------------------------------------------- 1 | use web_extensions::tabs::*; 2 | 3 | mod util; 4 | use util::*; 5 | 6 | #[test] 7 | fn status_serde() { 8 | assert_json_serde_test_cases(&[ 9 | JSONSerdeTestCase { 10 | value: Status::Loading, 11 | json: r#""loading""#, 12 | }, 13 | JSONSerdeTestCase { 14 | value: Status::Complete, 15 | json: r#""complete""#, 16 | }, 17 | ]) 18 | } 19 | 20 | #[test] 21 | fn window_type_serialize() { 22 | assert_json_serialize_test_cases(&[ 23 | JSONSerdeTestCase { 24 | value: WindowType::Normal, 25 | json: r#""normal""#, 26 | }, 27 | JSONSerdeTestCase { 28 | value: WindowType::Popup, 29 | json: r#""popup""#, 30 | }, 31 | JSONSerdeTestCase { 32 | value: WindowType::Panel, 33 | json: r#""panel""#, 34 | }, 35 | JSONSerdeTestCase { 36 | value: WindowType::Devtools, 37 | json: r#""devtools""#, 38 | }, 39 | ]) 40 | } 41 | -------------------------------------------------------------------------------- /tests/util/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::PartialEq, fmt::Debug}; 2 | 3 | pub fn assert_json_serialize_eq<'a, T>(left: &'a T, right: &'a str) 4 | where 5 | T: serde::Serialize + Debug, 6 | { 7 | assert_eq!( 8 | serde_json::to_string(left).expect(&format!("failed to serialize {:?} to JSON", left)), 9 | right 10 | ); 11 | } 12 | 13 | pub fn assert_json_deserialize_eq<'a, T>(left: &'a str, right: &'a T) 14 | where 15 | T: serde::Deserialize<'a> + PartialEq + Debug, 16 | { 17 | assert_eq!( 18 | &serde_json::from_str::(left).expect(&format!("failed to deserialize JSON {}", left)), 19 | right 20 | ) 21 | } 22 | 23 | pub fn assert_json_serde_eq<'a, T>(left: &'a T, right: &'a str) 24 | where 25 | T: serde::Serialize + serde::Deserialize<'a> + PartialEq + Debug, 26 | { 27 | assert_json_serialize_eq(left, right); 28 | assert_json_deserialize_eq(right, left); 29 | } 30 | 31 | pub struct JSONSerdeTestCase<'a, T> { 32 | pub value: T, 33 | pub json: &'a str, 34 | } 35 | 36 | #[allow(dead_code)] 37 | pub fn assert_json_serialize_test_cases<'a, T, I>(tcs: I) 38 | where 39 | T: 'a + serde::Serialize + PartialEq + Debug, 40 | I: 'a + IntoIterator>, 41 | { 42 | for tc in tcs { 43 | assert_json_serialize_eq(&tc.value, tc.json); 44 | } 45 | } 46 | 47 | pub fn assert_json_serde_test_cases<'a, T, I>(tcs: I) 48 | where 49 | T: 'a + serde::Serialize + serde::Deserialize<'a> + PartialEq + Debug, 50 | I: 'a + IntoIterator>, 51 | { 52 | for tc in tcs { 53 | assert_json_serde_eq(&tc.value, tc.json); 54 | } 55 | } 56 | --------------------------------------------------------------------------------