#![cfg(wasm)]
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
pub use reinhardt_pages::testing::{
MockResponse, assert_server_fn_call_count, assert_server_fn_called,
assert_server_fn_called_with, assert_server_fn_not_called, clear_mocks, get_call_history,
};
#[derive(Debug, Clone, Default)]
pub struct MockStorage {
data: Rc<RefCell<HashMap<String, String>>>,
}
impl MockStorage {
pub fn new() -> Self {
Self::default()
}
pub fn with_data(data: HashMap<String, String>) -> Self {
Self {
data: Rc::new(RefCell::new(data)),
}
}
pub fn length(&self) -> usize {
self.data.borrow().len()
}
pub fn get_item(&self, key: &str) -> Option<String> {
self.data.borrow().get(key).cloned()
}
pub fn set_item(&self, key: &str, value: &str) {
self.data
.borrow_mut()
.insert(key.to_string(), value.to_string());
}
pub fn remove_item(&self, key: &str) {
self.data.borrow_mut().remove(key);
}
pub fn clear(&self) {
self.data.borrow_mut().clear();
}
pub fn key(&self, index: usize) -> Option<String> {
self.data.borrow().keys().nth(index).cloned()
}
pub fn keys(&self) -> Vec<String> {
self.data.borrow().keys().cloned().collect()
}
pub fn values(&self) -> Vec<String> {
self.data.borrow().values().cloned().collect()
}
pub fn entries(&self) -> Vec<(String, String)> {
self.data
.borrow()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn contains_key(&self, key: &str) -> bool {
self.data.borrow().contains_key(key)
}
}
#[derive(Debug, Clone, Default)]
pub struct MockCookies {
cookies: Rc<RefCell<HashMap<String, CookieEntry>>>,
}
#[derive(Debug, Clone)]
pub struct CookieEntry {
pub value: String,
pub options: CookieOptions,
}
#[derive(Debug, Clone, Default)]
pub struct CookieOptions {
pub max_age: Option<i64>,
pub expires: Option<i64>,
pub path: Option<String>,
pub domain: Option<String>,
pub secure: bool,
pub http_only: bool,
pub same_site: Option<SameSite>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SameSite {
Strict,
Lax,
None,
}
impl MockCookies {
pub fn new() -> Self {
Self::default()
}
pub fn with_cookies(cookies: HashMap<String, String>) -> Self {
let entries: HashMap<String, CookieEntry> = cookies
.into_iter()
.map(|(k, v)| {
(
k,
CookieEntry {
value: v,
options: CookieOptions::default(),
},
)
})
.collect();
Self {
cookies: Rc::new(RefCell::new(entries)),
}
}
pub fn get(&self, name: &str) -> Option<String> {
self.cookies
.borrow()
.get(name)
.map(|entry| entry.value.clone())
}
pub fn get_entry(&self, name: &str) -> Option<CookieEntry> {
self.cookies.borrow().get(name).cloned()
}
pub fn set(&self, name: &str, value: &str) {
self.set_with_options(name, value, CookieOptions::default());
}
pub fn set_with_options(&self, name: &str, value: &str, options: CookieOptions) {
self.cookies.borrow_mut().insert(
name.to_string(),
CookieEntry {
value: value.to_string(),
options,
},
);
}
pub fn remove(&self, name: &str) {
self.cookies.borrow_mut().remove(name);
}
pub fn clear(&self) {
self.cookies.borrow_mut().clear();
}
pub fn names(&self) -> Vec<String> {
self.cookies.borrow().keys().cloned().collect()
}
pub fn all(&self) -> HashMap<String, String> {
self.cookies
.borrow()
.iter()
.map(|(k, v)| (k.clone(), v.value.clone()))
.collect()
}
pub fn has(&self, name: &str) -> bool {
self.cookies.borrow().contains_key(name)
}
pub fn len(&self) -> usize {
self.cookies.borrow().len()
}
pub fn is_empty(&self) -> bool {
self.cookies.borrow().is_empty()
}
pub fn to_cookie_string(&self) -> String {
self.cookies
.borrow()
.iter()
.map(|(name, entry)| format!("{}={}", name, entry.value))
.collect::<Vec<_>>()
.join("; ")
}
}
#[derive(Debug, Default)]
pub struct MockTimers {
callbacks: Rc<RefCell<Vec<TimerCallback>>>,
current_time: Rc<RefCell<f64>>,
}
struct TimerCallback {
id: u32,
callback: Box<dyn FnOnce()>,
scheduled_time: f64,
is_interval: bool,
interval_ms: Option<u32>,
}
impl std::fmt::Debug for TimerCallback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TimerCallback")
.field("id", &self.id)
.field("callback", &"<FnOnce>")
.field("scheduled_time", &self.scheduled_time)
.field("is_interval", &self.is_interval)
.field("interval_ms", &self.interval_ms)
.finish()
}
}
impl MockTimers {
pub fn new() -> Self {
Self::default()
}
pub fn now(&self) -> f64 {
*self.current_time.borrow()
}
pub fn advance_by(&self, ms: u32) {
let new_time = *self.current_time.borrow() + ms as f64;
*self.current_time.borrow_mut() = new_time;
self.run_due_callbacks();
}
pub fn run_all(&self) {
let max_time = self
.callbacks
.borrow()
.iter()
.map(|cb| cb.scheduled_time)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
if max_time > *self.current_time.borrow() {
*self.current_time.borrow_mut() = max_time;
self.run_due_callbacks();
}
}
pub fn pending_count(&self) -> usize {
self.callbacks.borrow().len()
}
pub fn clear_all(&self) {
self.callbacks.borrow_mut().clear();
}
fn run_due_callbacks(&self) {
let current = *self.current_time.borrow();
let all_callbacks: Vec<TimerCallback> = self.callbacks.borrow_mut().drain(..).collect();
let mut remaining = Vec::new();
let mut due = Vec::new();
for cb in all_callbacks {
if cb.scheduled_time <= current {
due.push(cb);
} else {
remaining.push(cb);
}
}
*self.callbacks.borrow_mut() = remaining;
due.sort_by(|a, b| {
a.scheduled_time
.partial_cmp(&b.scheduled_time)
.unwrap_or(std::cmp::Ordering::Equal)
});
for cb in due {
(cb.callback)();
}
}
}
#[derive(Debug)]
pub struct MutationTracker {
mutations: Rc<RefCell<Vec<MutationRecord>>>,
}
#[derive(Debug, Clone)]
pub struct MutationRecord {
pub mutation_type: String,
pub target: String,
pub attribute_name: Option<String>,
pub old_value: Option<String>,
pub added_nodes_count: usize,
pub removed_nodes_count: usize,
}
impl MutationTracker {
pub fn new(_element: &web_sys::Element) -> Self {
unimplemented!(
"MutationTracker requires a WASM runtime with browser DOM access. \
MutationObserver cannot be set up outside of an actual browser environment."
)
}
pub fn mutations(&self) -> Vec<MutationRecord> {
self.mutations.borrow().clone()
}
pub fn clear(&self) {
self.mutations.borrow_mut().clear();
}
pub fn has_mutations(&self) -> bool {
!self.mutations.borrow().is_empty()
}
pub fn mutation_count(&self) -> usize {
self.mutations.borrow().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
fn test_mock_storage_operations() {
let storage = MockStorage::new();
assert_eq!(storage.length(), 0);
storage.set_item("key1", "value1");
storage.set_item("key2", "value2");
assert_eq!(storage.length(), 2);
assert_eq!(storage.get_item("key1"), Some("value1".to_string()));
assert_eq!(storage.get_item("key2"), Some("value2".to_string()));
assert_eq!(storage.get_item("key3"), None);
storage.remove_item("key1");
assert_eq!(storage.length(), 1);
assert_eq!(storage.get_item("key1"), None);
storage.clear();
assert_eq!(storage.length(), 0);
}
#[rstest]
fn test_mock_storage_with_data() {
let mut data = HashMap::new();
data.insert("a".to_string(), "1".to_string());
data.insert("b".to_string(), "2".to_string());
let storage = MockStorage::with_data(data);
assert_eq!(storage.length(), 2);
assert_eq!(storage.get_item("a"), Some("1".to_string()));
assert_eq!(storage.get_item("b"), Some("2".to_string()));
}
#[rstest]
fn test_mock_storage_key_by_index() {
let storage = MockStorage::new();
storage.set_item("alpha", "val");
let key_0 = storage.key(0);
let key_out_of_bounds = storage.key(99);
assert_eq!(key_0, Some("alpha".to_string()));
assert_eq!(key_out_of_bounds, None);
}
#[rstest]
fn test_mock_storage_keys_values_entries() {
let storage = MockStorage::new();
storage.set_item("x", "10");
storage.set_item("y", "20");
let keys = storage.keys();
let values = storage.values();
let entries = storage.entries();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"x".to_string()));
assert!(keys.contains(&"y".to_string()));
assert_eq!(values.len(), 2);
assert!(values.contains(&"10".to_string()));
assert!(values.contains(&"20".to_string()));
assert_eq!(entries.len(), 2);
}
#[rstest]
fn test_mock_storage_contains_key() {
let storage = MockStorage::new();
storage.set_item("present", "yes");
assert!(storage.contains_key("present"));
assert!(!storage.contains_key("absent"));
}
#[rstest]
fn test_mock_storage_length_after_ops() {
let storage = MockStorage::new();
assert_eq!(storage.length(), 0);
storage.set_item("a", "1");
assert_eq!(storage.length(), 1);
storage.set_item("b", "2");
assert_eq!(storage.length(), 2);
storage.remove_item("a");
assert_eq!(storage.length(), 1);
storage.clear();
assert_eq!(storage.length(), 0);
}
#[rstest]
fn test_mock_storage_overwrite() {
let storage = MockStorage::new();
storage.set_item("key", "old_value");
storage.set_item("key", "new_value");
assert_eq!(storage.length(), 1);
assert_eq!(storage.get_item("key"), Some("new_value".to_string()));
}
#[rstest]
fn test_mock_storage_remove_nonexistent() {
let storage = MockStorage::new();
storage.set_item("key", "value");
storage.remove_item("nonexistent");
assert_eq!(storage.length(), 1);
assert_eq!(storage.get_item("key"), Some("value".to_string()));
}
#[rstest]
fn test_mock_storage_empty_key_and_value() {
let storage = MockStorage::new();
storage.set_item("", "empty_key");
storage.set_item("empty_value", "");
assert_eq!(storage.get_item(""), Some("empty_key".to_string()));
assert_eq!(storage.get_item("empty_value"), Some(String::new()));
assert_eq!(storage.length(), 2);
}
#[rstest]
fn test_mock_storage_clone_independence() {
let storage = MockStorage::new();
storage.set_item("shared", "data");
let cloned = storage.clone();
cloned.set_item("extra", "value");
assert_eq!(storage.length(), 2);
assert_eq!(storage.get_item("extra"), Some("value".to_string()));
}
#[rstest]
fn test_mock_cookies_operations() {
let cookies = MockCookies::new();
assert!(cookies.is_empty());
cookies.set("session", "abc123");
cookies.set_with_options(
"auth",
"xyz",
CookieOptions {
secure: true,
http_only: true,
..Default::default()
},
);
assert_eq!(cookies.len(), 2);
assert_eq!(cookies.get("session"), Some("abc123".to_string()));
assert_eq!(cookies.get("auth"), Some("xyz".to_string()));
let entry = cookies.get_entry("auth").unwrap();
assert!(entry.options.secure);
assert!(entry.options.http_only);
cookies.remove("session");
assert!(!cookies.has("session"));
assert!(cookies.has("auth"));
}
#[rstest]
fn test_mock_cookies_with_cookies_constructor() {
let mut initial = HashMap::new();
initial.insert("a".to_string(), "1".to_string());
initial.insert("b".to_string(), "2".to_string());
let cookies = MockCookies::with_cookies(initial);
assert_eq!(cookies.len(), 2);
assert_eq!(cookies.get("a"), Some("1".to_string()));
assert_eq!(cookies.get("b"), Some("2".to_string()));
}
#[rstest]
fn test_mock_cookies_names_and_all() {
let cookies = MockCookies::new();
cookies.set("x", "10");
cookies.set("y", "20");
let names = cookies.names();
let all = cookies.all();
assert_eq!(names.len(), 2);
assert!(names.contains(&"x".to_string()));
assert!(names.contains(&"y".to_string()));
assert_eq!(all.len(), 2);
assert_eq!(all.get("x"), Some(&"10".to_string()));
assert_eq!(all.get("y"), Some(&"20".to_string()));
}
#[rstest]
fn test_mock_cookies_is_empty_transitions() {
let cookies = MockCookies::new();
assert!(cookies.is_empty());
cookies.set("k", "v");
assert!(!cookies.is_empty());
cookies.remove("k");
assert!(cookies.is_empty());
}
#[rstest]
fn test_mock_cookies_get_entry_with_options() {
let cookies = MockCookies::new();
cookies.set_with_options(
"tracking",
"abc",
CookieOptions {
max_age: Some(3600),
path: Some("/app".to_string()),
domain: Some("example.com".to_string()),
secure: true,
http_only: false,
same_site: Some(SameSite::Lax),
expires: None,
},
);
let entry = cookies.get_entry("tracking").unwrap();
assert_eq!(entry.value, "abc");
assert_eq!(entry.options.max_age, Some(3600));
assert_eq!(entry.options.path, Some("/app".to_string()));
assert_eq!(entry.options.domain, Some("example.com".to_string()));
assert!(entry.options.secure);
assert!(!entry.options.http_only);
assert_eq!(entry.options.same_site, Some(SameSite::Lax));
}
#[rstest]
fn test_mock_cookies_get_entry_nonexistent() {
let cookies = MockCookies::new();
let entry = cookies.get_entry("missing");
assert!(entry.is_none());
}
#[rstest]
fn test_mock_cookies_to_cookie_string_format() {
let mut initial = HashMap::new();
initial.insert("a".to_string(), "1".to_string());
let cookies = MockCookies::with_cookies(initial);
let cookie_str = cookies.to_cookie_string();
assert_eq!(cookie_str, "a=1");
}
#[rstest]
fn test_mock_cookies_to_cookie_string_empty() {
let cookies = MockCookies::new();
let cookie_str = cookies.to_cookie_string();
assert_eq!(cookie_str, "");
}
#[rstest]
fn test_mock_cookies_to_cookie_string_multiple() {
let cookies = MockCookies::new();
cookies.set("a", "1");
cookies.set("b", "2");
let cookie_str = cookies.to_cookie_string();
assert!(cookie_str.contains("a=1"));
assert!(cookie_str.contains("b=2"));
assert!(cookie_str.contains("; "));
}
#[rstest]
fn test_cookie_options_default() {
let opts = CookieOptions::default();
assert_eq!(opts.max_age, None);
assert_eq!(opts.expires, None);
assert_eq!(opts.path, None);
assert_eq!(opts.domain, None);
assert!(!opts.secure);
assert!(!opts.http_only);
assert!(opts.same_site.is_none());
}
#[rstest]
fn test_same_site_variants() {
assert_eq!(SameSite::Strict, SameSite::Strict);
assert_eq!(SameSite::Lax, SameSite::Lax);
assert_eq!(SameSite::None, SameSite::None);
assert_ne!(SameSite::Strict, SameSite::Lax);
assert_ne!(SameSite::Lax, SameSite::None);
assert_ne!(SameSite::Strict, SameSite::None);
}
#[rstest]
fn test_mock_cookies_clear() {
let cookies = MockCookies::new();
cookies.set("a", "1");
cookies.set("b", "2");
assert_eq!(cookies.len(), 2);
cookies.clear();
assert_eq!(cookies.len(), 0);
assert!(cookies.is_empty());
}
#[rstest]
fn test_mock_timers_initial_state() {
let timers = MockTimers::new();
assert_eq!(timers.now(), 0.0);
assert_eq!(timers.pending_count(), 0);
}
#[rstest]
fn test_mock_timers_advance_by() {
let timers = MockTimers::new();
timers.advance_by(100);
assert_eq!(timers.now(), 100.0);
timers.advance_by(50);
assert_eq!(timers.now(), 150.0);
}
#[rstest]
fn test_mock_timers_clear_all() {
let timers = MockTimers::new();
timers.clear_all();
assert_eq!(timers.pending_count(), 0);
}
}