Compare commits
2 commits
55cfc1329c
...
8cfef276df
Author | SHA1 | Date | |
---|---|---|---|
8cfef276df | |||
462048db2f |
3 changed files with 278 additions and 2 deletions
|
@ -8,4 +8,8 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=info
|
- RUST_LOG=info
|
||||||
- LEPTOS_ENV=production
|
- LEPTOS_ENV=production
|
||||||
|
- LEPTOS_SITE_ADDR=0.0.0.0:3004
|
||||||
|
- LEPTOS_SITE_ROOT=site
|
||||||
|
- LEPTOS_OUTPUT_NAME=compareware
|
||||||
|
- LEPTOS_OPTIONS={"site_addr":"0.0.0.0:3004"}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
|
@ -28,6 +28,8 @@ COPY . .
|
||||||
RUN rustup target add wasm32-unknown-unknown
|
RUN rustup target add wasm32-unknown-unknown
|
||||||
# Build project
|
# Build project
|
||||||
ENV LEPTOS_OUTPUT_NAME="compareware"
|
ENV LEPTOS_OUTPUT_NAME="compareware"
|
||||||
|
ENV LEPTOS_SITE_ADDR="0.0.0.0:3004"
|
||||||
|
ENV LEPTOS_SITE_ROOT="site"
|
||||||
|
|
||||||
# Build with release profile
|
# Build with release profile
|
||||||
RUN cargo leptos build --release
|
RUN cargo leptos build --release
|
||||||
|
@ -51,6 +53,7 @@ COPY assets /app/assets
|
||||||
# Configure container, expose port and set entrypoint
|
# Configure container, expose port and set entrypoint
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 3004
|
EXPOSE 3004
|
||||||
ENV LEPTOS_SITE_ADDR=0.0.0.0:3004
|
ENV LEPTOS_SITE_ADDR="0.0.0.0:3004"
|
||||||
ENV LEPTOS_SITE_ROOT="site"
|
ENV LEPTOS_SITE_ROOT="site"
|
||||||
|
ENV LEPTOS_OPTIONS='{"site_addr":"0.0.0.0:3004"}'
|
||||||
CMD ["./compareware"]
|
CMD ["./compareware"]
|
|
@ -6,6 +6,9 @@ use gloo_timers::future::sleep;
|
||||||
use compareware::components::typeahead_input::TypeaheadInput;
|
use compareware::components::typeahead_input::TypeaheadInput;
|
||||||
use compareware::models::item::WikidataSuggestion;
|
use compareware::models::item::WikidataSuggestion;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use gloo_timers::future::TimeoutFuture;
|
||||||
|
|
||||||
// Import mock module
|
// Import mock module
|
||||||
mod mocks;
|
mod mocks;
|
||||||
|
@ -269,6 +272,272 @@ async fn test_rapid_mount_unmount() {
|
||||||
document.body().unwrap().remove_child(&container).unwrap();
|
document.body().unwrap().remove_child(&container).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Memory Leak Regression Test
|
||||||
|
|
||||||
|
/// Helper to count registry entries and global handlers in JS
|
||||||
|
async fn get_js_leak_stats() -> (u32, u32) {
|
||||||
|
let js = r#"
|
||||||
|
(function() {
|
||||||
|
let reg = window.typeaheadRegistry;
|
||||||
|
let regCount = reg ? Object.keys(reg).length : 0;
|
||||||
|
let handlerCount = 0;
|
||||||
|
for (let k in window) {
|
||||||
|
if (k.startsWith('rustSelectHandler_') || k.startsWith('rustFetchHandler_')) {
|
||||||
|
if (window[k]) handlerCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [regCount, handlerCount];
|
||||||
|
})()
|
||||||
|
"#;
|
||||||
|
let arr = js_sys::eval(js).unwrap();
|
||||||
|
let arr = js_sys::Array::from(&arr);
|
||||||
|
let reg_count = arr.get(0).as_f64().unwrap_or(0.0) as u32;
|
||||||
|
let handler_count = arr.get(1).as_f64().unwrap_or(0.0) as u32;
|
||||||
|
(reg_count, handler_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn test_typeahead_memory_leak_on_rapid_cycles() {
|
||||||
|
// Setup test environment
|
||||||
|
setup_test_environment().await;
|
||||||
|
|
||||||
|
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("leak-test-container");
|
||||||
|
|
||||||
|
// Run several mount/unmount cycles
|
||||||
|
let cycles = 10;
|
||||||
|
for i in 0..cycles {
|
||||||
|
log!("Mount/unmount cycle {}", i);
|
||||||
|
|
||||||
|
let test_component = move || {
|
||||||
|
let node_ref = create_node_ref::<leptos::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>
|
||||||
|
}.into_view()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mount
|
||||||
|
let unmount = mount_to(&container, test_component);
|
||||||
|
|
||||||
|
// Wait briefly for JS initialization
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
// Unmount
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
// Wait for cleanup
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for any pending JS cleanup
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Check for leaks
|
||||||
|
let (reg_count, handler_count) = get_js_leak_stats().await;
|
||||||
|
log!("After {} cycles: registry entries = {}, handler count = {}", cycles, reg_count, handler_count);
|
||||||
|
|
||||||
|
// Assert no registry entries or global handlers remain
|
||||||
|
assert_eq!(reg_count, 0, "JS registry should be empty after cleanup");
|
||||||
|
assert_eq!(handler_count, 0, "No global handlers should remain after cleanup");
|
||||||
|
|
||||||
|
// Cleanup DOM
|
||||||
|
document.body().unwrap().remove_child(&container).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle tests
|
||||||
|
fn create_mock_suggestion() -> compareware::models::item::WikidataSuggestion {
|
||||||
|
compareware::models::item::WikidataSuggestion {
|
||||||
|
id: "Q12345".to_string(),
|
||||||
|
label: "Test Item".to_string(),
|
||||||
|
title: "Test Title".to_string(),
|
||||||
|
description: "Test Description".to_string(),
|
||||||
|
display: compareware::models::item::DisplayInfo {
|
||||||
|
label: compareware::models::item::LabelInfo {
|
||||||
|
value: "Test Item".to_string(),
|
||||||
|
language: "en".to_string(),
|
||||||
|
},
|
||||||
|
description: compareware::models::item::DescriptionInfo {
|
||||||
|
value: "Test Description".to_string(),
|
||||||
|
language: "en".to_string(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a test component
|
||||||
|
fn create_test_component() -> (NodeRef<leptos::html::Input>, Rc<RefCell<Option<WikidataSuggestion>>>) {
|
||||||
|
let node_ref = create_node_ref::<leptos::html::Input>();
|
||||||
|
let selected_suggestion = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
|
let selected_suggestion_clone = selected_suggestion.clone();
|
||||||
|
let on_select = Callback::new(move |suggestion: WikidataSuggestion| {
|
||||||
|
*selected_suggestion_clone.borrow_mut() = Some(suggestion);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fetch_suggestions = Callback::new(|query: String| {
|
||||||
|
if query.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![create_mock_suggestion()]
|
||||||
|
});
|
||||||
|
|
||||||
|
mount_to_body(move || {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<TypeaheadInput
|
||||||
|
value="".to_string()
|
||||||
|
on_select=on_select
|
||||||
|
fetch_suggestions=fetch_suggestions
|
||||||
|
node_ref=node_ref
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(node_ref, selected_suggestion)
|
||||||
|
}
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn test_multiple_component_lifecycle() {
|
||||||
|
// Create first component
|
||||||
|
let (node_ref1, _) = create_test_component();
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
TimeoutFuture::new(500).await;
|
||||||
|
|
||||||
|
// Create second component
|
||||||
|
let (node_ref2, _) = create_test_component();
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
TimeoutFuture::new(500).await;
|
||||||
|
|
||||||
|
// Both should be initialized
|
||||||
|
assert!(node_ref1.get().is_some());
|
||||||
|
assert!(node_ref2.get().is_some());
|
||||||
|
|
||||||
|
// Clean up first component explicitly
|
||||||
|
leptos::document().body().unwrap().first_element_child().unwrap().remove();
|
||||||
|
|
||||||
|
// Wait for cleanup
|
||||||
|
TimeoutFuture::new(500).await;
|
||||||
|
|
||||||
|
// Second component should still be valid
|
||||||
|
assert!(node_ref2.get().is_some());
|
||||||
|
|
||||||
|
// Check if the registry has been properly maintained
|
||||||
|
let js_check = js_sys::eval(r#"
|
||||||
|
// Check if typeahead registry exists and has components
|
||||||
|
if (window.typeaheadRegistry) {
|
||||||
|
// Count alive components
|
||||||
|
let aliveCount = 0;
|
||||||
|
for (let id in window.typeaheadRegistry) {
|
||||||
|
if (window.typeaheadRegistry[id].alive) {
|
||||||
|
aliveCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aliveCount;
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
"#).unwrap();
|
||||||
|
|
||||||
|
// Should have exactly one alive component
|
||||||
|
assert_eq!(js_check.as_f64().unwrap_or(0.0), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn test_component_refresh_scenario() {
|
||||||
|
// This test simulates the scenario where components are refreshed
|
||||||
|
|
||||||
|
// First round of components
|
||||||
|
let components = (0..3).map(|_| create_test_component()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
TimeoutFuture::new(500).await;
|
||||||
|
|
||||||
|
// Verify all are initialized
|
||||||
|
for (node_ref, _) in &components {
|
||||||
|
assert!(node_ref.get().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up all components
|
||||||
|
leptos::document()
|
||||||
|
.body()
|
||||||
|
.expect("document should have a body")
|
||||||
|
.dyn_ref::<web_sys::HtmlElement>()
|
||||||
|
.expect("body should be an HtmlElement")
|
||||||
|
.set_inner_html("");
|
||||||
|
|
||||||
|
// Wait for cleanup
|
||||||
|
TimeoutFuture::new(500).await;
|
||||||
|
|
||||||
|
// Create new components
|
||||||
|
let new_components = (0..3).map(|_| create_test_component()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Wait for initialization
|
||||||
|
TimeoutFuture::new(500).await;
|
||||||
|
|
||||||
|
// Verify all new components are initialized
|
||||||
|
for (node_ref, _) in &new_components {
|
||||||
|
assert!(node_ref.get().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check registry health
|
||||||
|
let js_check = js_sys::eval(r#"
|
||||||
|
// Check registry health
|
||||||
|
let result = {
|
||||||
|
registryExists: !!window.typeaheadRegistry,
|
||||||
|
totalComponents: 0,
|
||||||
|
aliveComponents: 0,
|
||||||
|
globalHandlers: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.typeaheadRegistry) {
|
||||||
|
result.totalComponents = Object.keys(window.typeaheadRegistry).length;
|
||||||
|
|
||||||
|
for (let id in window.typeaheadRegistry) {
|
||||||
|
if (window.typeaheadRegistry[id].alive) {
|
||||||
|
result.aliveComponents++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count global handlers
|
||||||
|
for (let key in window) {
|
||||||
|
if (key.startsWith('rustSelectHandler_') || key.startsWith('rustFetchHandler_')) {
|
||||||
|
result.globalHandlers++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JSON.stringify(result);
|
||||||
|
"#).unwrap();
|
||||||
|
|
||||||
|
let registry_info = js_check.as_string().unwrap();
|
||||||
|
console_log!("Registry health: {}", registry_info);
|
||||||
|
|
||||||
|
// Parse the JSON
|
||||||
|
let registry_obj: serde_json::Value = serde_json::from_str(®istry_info).unwrap();
|
||||||
|
|
||||||
|
// Verify registry health
|
||||||
|
assert!(registry_obj["registryExists"].as_bool().unwrap());
|
||||||
|
assert_eq!(registry_obj["aliveComponents"].as_i64().unwrap(), 3);
|
||||||
|
assert_eq!(registry_obj["totalComponents"].as_i64().unwrap(), 3);
|
||||||
|
|
||||||
|
// Global handlers should match the number of components
|
||||||
|
assert_eq!(registry_obj["globalHandlers"].as_i64().unwrap(), 6); // 2 handlers per component
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to mount a component to a container
|
// Helper function to mount a component to a container
|
||||||
fn mount_to(
|
fn mount_to(
|
||||||
container: &web_sys::Element,
|
container: &web_sys::Element,
|
||||||
|
|
Loading…
Add table
Reference in a new issue