Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/python/impactx/dashboard/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
License: BSD-3-Clause-LBNL
"""

import asyncio

from . import server, state
from .app import application
from .Input.defaults import DashboardDefaults
Expand Down Expand Up @@ -42,6 +44,18 @@ class DashboardApp:

def start(self):
setup_dashboard()

# Ensure an event loop exists for Python 3.10+ (required on macOS)
# This prevents RuntimeError when server.start() tries to get_event_loop()
# In Python 3.10+, get_event_loop() raises RuntimeError if no loop exists
# We create and set one if needed
try:
asyncio.get_event_loop()
except RuntimeError:
# No event loop exists in this thread, create and set one
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

server.start()
return 0

Expand Down
16 changes: 15 additions & 1 deletion tests/python/dashboard/test_python_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ def test_python_import(dashboard):
"""
dashboard.load_example("testdata/example.py", manual=True)

# Wait for the file to finish loading and processing
# The importing_file state is True while loading, False when done
dashboard.assert_state("importing_file", False)

BEAM_PARAMETERS = [
("tracking_mode", "Particle Tracking"),
("space_charge", "false"),
Expand Down Expand Up @@ -72,7 +76,17 @@ def test_python_import(dashboard):

# Check input values
for element_id, expected_value in DISTRIBUTION_VALUES + LATTICE_CONFIGURATION:
actual_value = float(dashboard.sb.get_value(element_id))
# Wait for element to be present in the DOM (doesn't require visibility)
# This is important for CI environments where rendering may be slower
dashboard.sb.wait_for_element_present(element_id, timeout=10)

# Wait for the element's value to be populated
# This checks both trame state and DOM element (more reliable on CI)
dashboard.wait_for_element_value(element_id, timeout=10)

# Get the value - tries state first, then DOM element
# This is more reliable when DOM updates lag behind state updates
actual_value = float(dashboard.get_element_value(element_id))
assert actual_value == pytest.approx(expected_value, **APPROX_TOL), (
f"{element_id}: expected {expected_value}, got {actual_value}"
)
91 changes: 84 additions & 7 deletions tests/python/dashboard/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ def wait_for_interaction_ready(sb, timeout=TIMEOUT):
https://github.com/Kitware/trame-client/blob/master/trame_client/utils/testing.py#L132

"""
for i in range(timeout):
print(f"Waiting for dashboard to load - ({i + 1}s elapsed)")
if sb.is_element_present(".trame__loader"):
sb.sleep(1)
else:
print("Ready to interact with.")
return
print("Waiting for dashboard to load...")

# Wait for the dashboard to finish loading.
# This ensures all UI elements are rendered before we manipulate or check their values
sb.wait_for_element_present("#Input_route", timeout=10)

print("Ready to interact with.")


def wait_for_server_ready(process, timeout=TIMEOUT):
Expand Down Expand Up @@ -275,6 +275,83 @@ def get_state(self, state_name):
"""
return self.sb.execute_script(js_script, state_name)

def get_element_value(self, element_id: str):
"""
Get an element's value, trying multiple methods.

For distribution parameters and other nested state values, tries reading
from trame state first, then falls back to DOM element value attribute.
This is more reliable on slower CI environments where DOM updates lag.

:param element_id: ID of the input element (with or without # prefix).
:return: The value as a string.
"""
clean_id = element_id.lstrip("#")

# First, try reading from trame state (for nested parameters like distribution)
# Distribution parameters are in state.selected_distribution_parameters[name]["value"]
# Lattice parameters are in state.selected_lattice_list[index]["parameters"]
js_check_state = """
if (window.trame && window.trame.state) {
const state = window.trame.state;
const param_name = arguments[0];

// Check distribution parameters
if (state.selected_distribution_parameters &&
state.selected_distribution_parameters[param_name]) {
const value = state.selected_distribution_parameters[param_name].value;
if (value !== undefined && value !== null && value !== '') {
return String(value);
}
}
}
return null;
"""

try:
state_value = self.sb.execute_script(js_check_state, clean_id)
if state_value:
return state_value
except Exception:
pass

# Fall back to reading from DOM element
return self.sb.get_attribute(element_id, "value")

def wait_for_element_value(self, element_id: str, timeout=TIMEOUT):
"""
Wait for an element's value to be populated (non-empty).

This checks both trame state and DOM element value attribute.
This is useful after loading files, as DOM elements may take time to
reflect the updated state values, but state is updated immediately.

:param element_id: ID of the input element to check (with or without # prefix).
:param timeout: Maximum time to wait in seconds.
"""
for i in range(timeout):
try:
value = self.get_element_value(element_id)
if value and value.strip(): # Non-empty value
return value
except Exception:
pass

time.sleep(1)

# If we get here, try one more time to get the value (even if empty)
# to provide a better error message
try:
final_value = self.get_element_value(element_id)
raise TimeoutError(
f"Element '{element_id}' value never populated after {timeout} seconds "
f"(last value: '{final_value}')"
)
except Exception as e:
raise TimeoutError(
f"Element '{element_id}' value never populated after {timeout} seconds"
) from e


def save_failure_screenshot(
dashboard, request, directory: str | None = None
Expand Down
Loading