Compare commits
3 commits
e528f9e684
...
a2a1a48ea6
Author | SHA1 | Date | |
---|---|---|---|
a2a1a48ea6 | |||
46b6cf82e2 | |||
24c138b866 |
5 changed files with 382 additions and 15 deletions
|
@ -135,8 +135,16 @@ pub fn TypeaheadInput(
|
||||||
initialized: false,
|
initialized: false,
|
||||||
bloodhound: null,
|
bloodhound: null,
|
||||||
handlers: {{}},
|
handlers: {{}},
|
||||||
|
// EXPLICIT ALIVE FLAG
|
||||||
|
alive: true,
|
||||||
// Method to safely call handlers
|
// Method to safely call handlers
|
||||||
callHandler: function(handlerName, ...args) {{
|
callHandler: function(handlerName, ...args) {{
|
||||||
|
// DEFENSIVE: Check alive flag before calling any handler
|
||||||
|
if (!this.alive) {{
|
||||||
|
console.warn('[JS] Component {component_id} is no longer alive, ignoring handler call: ' + handlerName);
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
|
||||||
try {{
|
try {{
|
||||||
const handler = this.handlers[handlerName];
|
const handler = this.handlers[handlerName];
|
||||||
if (handler && typeof handler === 'function') {{
|
if (handler && typeof handler === 'function') {{
|
||||||
|
@ -149,6 +157,12 @@ pub fn TypeaheadInput(
|
||||||
}},
|
}},
|
||||||
// Method to register a handler
|
// Method to register a handler
|
||||||
registerHandler: function(name, fn) {{
|
registerHandler: function(name, fn) {{
|
||||||
|
// DEFENSIVE: Don't register handlers if component is not alive
|
||||||
|
if (!this.alive) {{
|
||||||
|
console.warn('[JS] Component {component_id} is no longer alive, ignoring handler registration: ' + name);
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
|
||||||
this.handlers[name] = fn;
|
this.handlers[name] = fn;
|
||||||
return true;
|
return true;
|
||||||
}},
|
}},
|
||||||
|
@ -163,11 +177,43 @@ pub fn TypeaheadInput(
|
||||||
// Method to clean up all resources
|
// Method to clean up all resources
|
||||||
cleanup: function() {{
|
cleanup: function() {{
|
||||||
try {{
|
try {{
|
||||||
|
// IMPORTANT: Set alive to false FIRST to prevent any new calls
|
||||||
|
this.alive = false;
|
||||||
|
console.log('[JS] Component {component_id} marked as not alive');
|
||||||
|
|
||||||
// Clean up typeahead
|
// Clean up typeahead
|
||||||
const inputId = 'typeahead-input-{component_id}';
|
const inputId = 'typeahead-input-{component_id}';
|
||||||
const $input = $('#' + inputId);
|
const $input = $('#' + inputId);
|
||||||
if ($input.length > 0) {{
|
if ($input.length > 0) {{
|
||||||
|
// Remove all event handlers first
|
||||||
|
$input.off('typeahead:select');
|
||||||
|
$input.off('typeahead:active');
|
||||||
|
$input.off('typeahead:idle');
|
||||||
|
$input.off('typeahead:open');
|
||||||
|
$input.off('typeahead:close');
|
||||||
|
$input.off('typeahead:change');
|
||||||
|
$input.off('typeahead:render');
|
||||||
|
$input.off('typeahead:autocomplete');
|
||||||
|
$input.off('typeahead:cursorchange');
|
||||||
|
$input.off('typeahead:asyncrequest');
|
||||||
|
$input.off('typeahead:asynccancel');
|
||||||
|
$input.off('typeahead:asyncreceive');
|
||||||
|
console.log('[JS] Removed all typeahead event handlers for {component_id}');
|
||||||
|
|
||||||
|
// Now destroy the typeahead
|
||||||
$input.typeahead('destroy');
|
$input.typeahead('destroy');
|
||||||
|
console.log('[JS] Destroyed typeahead for {component_id}');
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Explicitly null out the global handler references
|
||||||
|
if (window.rustSelectHandler_{component_id_safe}) {{
|
||||||
|
window.rustSelectHandler_{component_id_safe} = null;
|
||||||
|
console.log('[JS] Nulled rustSelectHandler_{component_id_safe}');
|
||||||
|
}}
|
||||||
|
|
||||||
|
if (window.rustFetchHandler_{component_id_safe}) {{
|
||||||
|
window.rustFetchHandler_{component_id_safe} = null;
|
||||||
|
console.log('[JS] Nulled rustFetchHandler_{component_id_safe}');
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Clear all handlers
|
// Clear all handlers
|
||||||
|
@ -181,6 +227,8 @@ pub fn TypeaheadInput(
|
||||||
return true;
|
return true;
|
||||||
}} catch (e) {{
|
}} catch (e) {{
|
||||||
console.error('[JS] Error during cleanup:', e);
|
console.error('[JS] Error during cleanup:', e);
|
||||||
|
// Still mark as not alive even if cleanup fails
|
||||||
|
this.alive = false;
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
@ -189,7 +237,8 @@ pub fn TypeaheadInput(
|
||||||
console.log('[JS] Registered component {component_id}');
|
console.log('[JS] Registered component {component_id}');
|
||||||
true
|
true
|
||||||
"#,
|
"#,
|
||||||
component_id = component_id
|
component_id = component_id,
|
||||||
|
component_id_safe = component_id.replace("-", "_")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute the registration script
|
// Execute the registration script
|
||||||
|
@ -342,20 +391,34 @@ pub fn TypeaheadInput(
|
||||||
r#"
|
r#"
|
||||||
// Perform cleanup in JavaScript first
|
// Perform cleanup in JavaScript first
|
||||||
if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{
|
if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{
|
||||||
|
console.log('[JS] Starting cleanup for component {component_id}');
|
||||||
|
|
||||||
// Clean up the component
|
// Clean up the component
|
||||||
const result = window.typeaheadRegistry['{component_id}'].cleanup();
|
const result = window.typeaheadRegistry['{component_id}'].cleanup();
|
||||||
|
|
||||||
|
// DEFENSIVE: Explicitly null out global handlers even if cleanup fails
|
||||||
|
if (window.rustSelectHandler_{component_id_safe}) {{
|
||||||
|
window.rustSelectHandler_{component_id_safe} = null;
|
||||||
|
console.log('[JS] Nulled rustSelectHandler_{component_id_safe} during cleanup');
|
||||||
|
}}
|
||||||
|
|
||||||
|
if (window.rustFetchHandler_{component_id_safe}) {{
|
||||||
|
window.rustFetchHandler_{component_id_safe} = null;
|
||||||
|
console.log('[JS] Nulled rustFetchHandler_{component_id_safe} during cleanup');
|
||||||
|
}}
|
||||||
|
|
||||||
// Remove from registry
|
// Remove from registry
|
||||||
delete window.typeaheadRegistry['{component_id}'];
|
delete window.typeaheadRegistry['{component_id}'];
|
||||||
|
|
||||||
console.log('[JS] Component {component_id} removed from registry');
|
console.log('[JS] Component {component_id} removed from registry');
|
||||||
result
|
result
|
||||||
}} else {{
|
}} else {{
|
||||||
console.warn('[JS] Component {component_id} not found in registry');
|
console.warn('[JS] Component {component_id} not found in registry during cleanup');
|
||||||
false
|
false
|
||||||
}}
|
}}
|
||||||
"#,
|
"#,
|
||||||
component_id = component_id_for_cleanup
|
component_id = component_id_for_cleanup,
|
||||||
|
component_id_safe = component_id_for_cleanup.replace("-", "_")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute JavaScript cleanup
|
// Execute JavaScript cleanup
|
||||||
|
@ -643,16 +706,25 @@ fn initialize_bloodhound(
|
||||||
let transport_fn = js_sys::Function::new_with_args(
|
let transport_fn = js_sys::Function::new_with_args(
|
||||||
"query, syncResults, asyncResults",
|
"query, syncResults, asyncResults",
|
||||||
&format!(r#"
|
&format!(r#"
|
||||||
|
// DEFENSIVE: Check if registry exists and component is alive
|
||||||
|
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
|
||||||
|
console.warn('[JS] Component registry not found for {component_id}, returning empty results');
|
||||||
|
syncResults([]);
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// DEFENSIVE: Check alive flag explicitly
|
||||||
|
if (!window.typeaheadRegistry['{component_id}'].alive) {{
|
||||||
|
console.warn('[JS] Component {component_id} is no longer alive, returning empty results');
|
||||||
|
syncResults([]);
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
// Call our handler through the registry
|
// Call our handler through the registry
|
||||||
if (window.typeaheadRegistry && window.typeaheadRegistry['{component_id}']) {{
|
try {{
|
||||||
try {{
|
window.typeaheadRegistry['{component_id}'].callHandler('fetch', query, syncResults, asyncResults);
|
||||||
window.typeaheadRegistry['{component_id}'].callHandler('fetch', query, syncResults, asyncResults);
|
}} catch (e) {{
|
||||||
}} catch (e) {{
|
console.error('[JS] Error calling fetch handler through registry:', e);
|
||||||
console.error('[JS] Error calling fetch handler through registry:', e);
|
|
||||||
syncResults([]);
|
|
||||||
}}
|
|
||||||
}} else {{
|
|
||||||
console.error('[JS] Component registry not found for {component_id}');
|
|
||||||
syncResults([]);
|
syncResults([]);
|
||||||
}}
|
}}
|
||||||
"#, component_id = component_id)
|
"#, component_id = component_id)
|
||||||
|
@ -908,11 +980,16 @@ fn initialize_typeahead(
|
||||||
r#"
|
r#"
|
||||||
console.log('[JS] Starting Typeahead init for #{input_id}');
|
console.log('[JS] Starting Typeahead init for #{input_id}');
|
||||||
try {{
|
try {{
|
||||||
// Get the component from registry
|
// DEFENSIVE: Check if registry exists and component is alive
|
||||||
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
|
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
|
||||||
throw new Error('Component not found in registry: {component_id}');
|
throw new Error('Component not found in registry: {component_id}');
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// DEFENSIVE: Check alive flag explicitly
|
||||||
|
if (!window.typeaheadRegistry['{component_id}'].alive) {{
|
||||||
|
throw new Error('Component {component_id} is no longer alive');
|
||||||
|
}}
|
||||||
|
|
||||||
// Get the bloodhound instance from the registry
|
// Get the bloodhound instance from the registry
|
||||||
var bloodhound = window.typeaheadRegistry['{component_id}'].bloodhound;
|
var bloodhound = window.typeaheadRegistry['{component_id}'].bloodhound;
|
||||||
if (!bloodhound) {{
|
if (!bloodhound) {{
|
||||||
|
@ -925,6 +1002,9 @@ fn initialize_typeahead(
|
||||||
throw new Error('Input element not found: #{input_id}');
|
throw new Error('Input element not found: #{input_id}');
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// DEFENSIVE: Remove any existing typeahead to prevent duplicate handlers
|
||||||
|
$input.typeahead('destroy');
|
||||||
|
|
||||||
$input.typeahead(
|
$input.typeahead(
|
||||||
{{
|
{{
|
||||||
hint: true,
|
hint: true,
|
||||||
|
@ -938,6 +1018,20 @@ fn initialize_typeahead(
|
||||||
return data.displayLabel || data.label || '';
|
return data.displayLabel || data.label || '';
|
||||||
}},
|
}},
|
||||||
source: function(query, syncResults, asyncResults) {{
|
source: function(query, syncResults, asyncResults) {{
|
||||||
|
// DEFENSIVE: Check if registry exists and component is alive
|
||||||
|
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
|
||||||
|
console.warn('[JS] Component registry not found for {component_id}, returning empty results');
|
||||||
|
syncResults([]);
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// DEFENSIVE: Check alive flag explicitly
|
||||||
|
if (!window.typeaheadRegistry['{component_id}'].alive) {{
|
||||||
|
console.warn('[JS] Component {component_id} is no longer alive, returning empty results');
|
||||||
|
syncResults([]);
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
// Call the fetch handler through the registry
|
// Call the fetch handler through the registry
|
||||||
window.typeaheadRegistry['{component_id}'].callHandler('fetch', query, syncResults, asyncResults);
|
window.typeaheadRegistry['{component_id}'].callHandler('fetch', query, syncResults, asyncResults);
|
||||||
}},
|
}},
|
||||||
|
@ -957,6 +1051,18 @@ fn initialize_typeahead(
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
.on('typeahead:select', function(ev, suggestion) {{
|
.on('typeahead:select', function(ev, suggestion) {{
|
||||||
|
// DEFENSIVE: Check if registry exists and component is alive
|
||||||
|
if (!window.typeaheadRegistry || !window.typeaheadRegistry['{component_id}']) {{
|
||||||
|
console.warn('[JS] Component registry not found for {component_id}, ignoring selection event');
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// DEFENSIVE: Check alive flag explicitly
|
||||||
|
if (!window.typeaheadRegistry['{component_id}'].alive) {{
|
||||||
|
console.warn('[JS] Component {component_id} is no longer alive, ignoring selection event');
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
if (!suggestion) {{
|
if (!suggestion) {{
|
||||||
console.error('[JS] Selection event received with null suggestion');
|
console.error('[JS] Selection event received with null suggestion');
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -5,6 +5,7 @@ use tokio::sync::Mutex;
|
||||||
use compareware::db::Database;
|
use compareware::db::Database;
|
||||||
use compareware::api::{ItemRequest, create_item, get_items, get_selected_properties, add_selected_property};
|
use compareware::api::{ItemRequest, create_item, get_items, get_selected_properties, add_selected_property};
|
||||||
use compareware::models::item::Item;
|
use compareware::models::item::Item;
|
||||||
|
use compareware::utils::panic_hook;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
|
@ -18,6 +19,8 @@ async fn main() -> std::io::Result<()> {
|
||||||
use compareware::api::{delete_item, delete_property}; // Import API handlers
|
use compareware::api::{delete_item, delete_property}; // Import API handlers
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
panic_hook::init();
|
||||||
|
|
||||||
// Setup logging
|
// Setup logging
|
||||||
std::env::set_var("RUST_LOG", "info");
|
std::env::set_var("RUST_LOG", "info");
|
||||||
|
@ -167,12 +170,15 @@ pub fn main() {
|
||||||
|
|
||||||
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
|
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
|
// Initialize custom panic hook for better diagnostics
|
||||||
|
panic_hook::init();
|
||||||
|
|
||||||
// a client-side main function is required for using `trunk serve`
|
// a client-side main function is required for using `trunk serve`
|
||||||
// prefer using `cargo leptos serve` instead
|
// prefer using `cargo leptos serve` instead
|
||||||
// to run: `trunk serve --open --features csr`
|
// to run: `trunk serve --open --features csr`
|
||||||
use compareware::app::*;
|
use compareware::app::*;
|
||||||
|
|
||||||
console_error_panic_hook::set_once();
|
// console_error_panic_hook::set_once();
|
||||||
|
|
||||||
leptos::mount_to_body(App);
|
leptos::mount_to_body(App);
|
||||||
}
|
}
|
|
@ -1 +1,2 @@
|
||||||
pub mod leptos_owner;
|
pub mod leptos_owner;
|
||||||
|
pub mod panic_hook;
|
54
src/utils/panic_hook.rs
Normal file
54
src/utils/panic_hook.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use std::panic;
|
||||||
|
use leptos::logging::log;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
/// Sets up a custom panic hook that provides more context for Leptos owner disposal panics
|
||||||
|
pub fn set_custom_panic_hook() {
|
||||||
|
let original_hook = panic::take_hook();
|
||||||
|
|
||||||
|
panic::set_hook(Box::new(move |panic_info| {
|
||||||
|
// Call the original hook first
|
||||||
|
original_hook(panic_info);
|
||||||
|
|
||||||
|
// Extract panic message
|
||||||
|
let message = if let Some(s) = panic_info.payload().downcast_ref::<String>() {
|
||||||
|
s.clone()
|
||||||
|
} else if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
||||||
|
s.to_string()
|
||||||
|
} else {
|
||||||
|
"Unknown panic".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if this is an owner disposal panic
|
||||||
|
if message.contains("OwnerDisposed") {
|
||||||
|
log!("[PANIC] Leptos owner disposal detected. This usually happens when:");
|
||||||
|
log!("[PANIC] 1. A component has been unmounted but JavaScript is still calling into Rust");
|
||||||
|
log!("[PANIC] 2. An effect or signal update is running after the component is gone");
|
||||||
|
log!("[PANIC] 3. A closure or callback is being called after cleanup");
|
||||||
|
|
||||||
|
// Log current component registry state
|
||||||
|
let js_code = r#"
|
||||||
|
if (window.typeaheadRegistry) {
|
||||||
|
console.log('[PANIC] Current typeahead registry:',
|
||||||
|
Object.keys(window.typeaheadRegistry).map(id => ({
|
||||||
|
id,
|
||||||
|
alive: window.typeaheadRegistry[id].alive,
|
||||||
|
initialized: window.typeaheadRegistry[id].initialized
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('[PANIC] No typeahead registry found');
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let _ = js_sys::eval(js_code);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call in main.rs or app initialization
|
||||||
|
pub fn init() {
|
||||||
|
log!("[PANIC_HOOK] Setting up custom panic hook");
|
||||||
|
set_custom_panic_hook();
|
||||||
|
log!("[PANIC_HOOK] Custom panic hook set up successfully");
|
||||||
|
}
|
200
tests/unit/typeahead_input_test.rs
Normal file
200
tests/unit/typeahead_input_test.rs
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
use wasm_bindgen_test::*;
|
||||||
|
use leptos::*;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
use std::time::Duration;
|
||||||
|
use gloo_timers::future::sleep;
|
||||||
|
use compareware::components::typeahead_input::TypeaheadInput;
|
||||||
|
use compareware::models::item::WikidataSuggestion;
|
||||||
|
|
||||||
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn test_typeahead_initialization() {
|
||||||
|
// Setup
|
||||||
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
|
let container = document.create_element("div").unwrap();
|
||||||
|
document.body().unwrap().append_child(&container).unwrap();
|
||||||
|
container.set_id("test-container");
|
||||||
|
|
||||||
|
// Track initialization
|
||||||
|
let init_called = create_rw_signal(false);
|
||||||
|
|
||||||
|
// Create a test component
|
||||||
|
let test_component = move || {
|
||||||
|
let node_ref = create_node_ref::<html::Input>();
|
||||||
|
|
||||||
|
// Mock callbacks
|
||||||
|
let on_select = Callback::new(move |suggestion: WikidataSuggestion| {
|
||||||
|
log!("Selected: {}", suggestion.label);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fetch_suggestions = Callback::new(move |query: String| {
|
||||||
|
log!("Fetching: {}", query);
|
||||||
|
init_called.set(true);
|
||||||
|
vec![]
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<TypeaheadInput
|
||||||
|
value="".to_string()
|
||||||
|
on_select=on_select
|
||||||
|
fetch_suggestions=fetch_suggestions
|
||||||
|
node_ref=node_ref
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mount the component
|
||||||
|
mount_to(&container, test_component);
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
for _ in 0..10 {
|
||||||
|
if init_called.get() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify initialization
|
||||||
|
assert!(init_called.get(), "Initialization callback was not called");
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body().unwrap().remove_child(&container).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn test_typeahead_cleanup() {
|
||||||
|
// Setup
|
||||||
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
|
let container = document.create_element("div").unwrap();
|
||||||
|
document.body().unwrap().append_child(&container).unwrap();
|
||||||
|
container.set_id("cleanup-test-container");
|
||||||
|
|
||||||
|
// Create a unique component ID for tracking
|
||||||
|
let component_id = format!("test-typeahead-{}", uuid::Uuid::new_v4());
|
||||||
|
|
||||||
|
// Create a test component
|
||||||
|
let test_component = move || {
|
||||||
|
let node_ref = create_node_ref::<html::Input>();
|
||||||
|
|
||||||
|
// Mock callbacks
|
||||||
|
let on_select = Callback::new(move |_: WikidataSuggestion| {});
|
||||||
|
let fetch_suggestions = Callback::new(move |_: String| vec![]);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<TypeaheadInput
|
||||||
|
value="".to_string()
|
||||||
|
on_select=on_select
|
||||||
|
fetch_suggestions=fetch_suggestions
|
||||||
|
node_ref=node_ref
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mount the component
|
||||||
|
let dispose = mount_to(&container, test_component);
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Check registry before unmount
|
||||||
|
let registry_before = js_sys::eval(&format!(
|
||||||
|
"window.typeaheadRegistry ? Object.keys(window.typeaheadRegistry).length : 0"
|
||||||
|
)).unwrap();
|
||||||
|
|
||||||
|
// Unmount the component
|
||||||
|
dispose();
|
||||||
|
|
||||||
|
// Wait for cleanup
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Check if component was properly removed from registry
|
||||||
|
let registry_after = js_sys::eval(&format!(
|
||||||
|
"window.typeaheadRegistry ? Object.keys(window.typeaheadRegistry).length : 0"
|
||||||
|
)).unwrap();
|
||||||
|
|
||||||
|
// Registry should have one fewer entry
|
||||||
|
assert!(
|
||||||
|
registry_before.as_f64().unwrap() > registry_after.as_f64().unwrap(),
|
||||||
|
"Component was not properly removed from registry"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body().unwrap().remove_child(&container).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn test_rapid_mount_unmount() {
|
||||||
|
// Setup
|
||||||
|
let document = web_sys::window().unwrap().document().unwrap();
|
||||||
|
let container = document.create_element("div").unwrap();
|
||||||
|
document.body().unwrap().append_child(&container).unwrap();
|
||||||
|
container.set_id("rapid-test-container");
|
||||||
|
|
||||||
|
// Perform rapid mount/unmount cycles to test for race conditions
|
||||||
|
for i in 0..5 {
|
||||||
|
log!("Mount/unmount cycle {}", i);
|
||||||
|
|
||||||
|
// Create a test component
|
||||||
|
let test_component = move || {
|
||||||
|
let node_ref = create_node_ref::<html::Input>();
|
||||||
|
let on_select = Callback::new(move |_: WikidataSuggestion| {});
|
||||||
|
let fetch_suggestions = Callback::new(move |_: String| vec![]);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<TypeaheadInput
|
||||||
|
value="".to_string()
|
||||||
|
on_select=on_select
|
||||||
|
fetch_suggestions=fetch_suggestions
|
||||||
|
node_ref=node_ref
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mount
|
||||||
|
let dispose = mount_to(&container, test_component);
|
||||||
|
|
||||||
|
// Wait briefly
|
||||||
|
sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
// Unmount
|
||||||
|
dispose();
|
||||||
|
|
||||||
|
// Wait briefly
|
||||||
|
sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for any pending cleanup
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Check if registry is clean
|
||||||
|
let registry_size = js_sys::eval(
|
||||||
|
"window.typeaheadRegistry ? Object.keys(window.typeaheadRegistry).length : 0"
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// Registry should be empty or at least not growing
|
||||||
|
assert!(
|
||||||
|
registry_size.as_f64().unwrap() < 5.0,
|
||||||
|
"Registry has too many entries after rapid mount/unmount cycles"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body().unwrap().remove_child(&container).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to mount a component to a container
|
||||||
|
fn mount_to(container: &web_sys::Element, component: impl FnOnce() -> View + 'static) -> impl FnOnce() {
|
||||||
|
let runtime = create_runtime();
|
||||||
|
let view = component();
|
||||||
|
leptos::mount_to_with_runtime(container, || view, runtime.clone());
|
||||||
|
|
||||||
|
move || {
|
||||||
|
runtime.dispose();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue