Solution UI testing#
Test the UI using init_dashclient#
The init_dashclient fixture sets up the Dash client environment and ensures that all API calls are routed
through the test infrastructure, allowing you to test Dash logic with real responses and no external dependencies.
All the examples below assume that you are testing a multi-page Dash application, which typically has a dash.register_page call at the top level of each page module. In this case, you need to use the dash.testing.ignore_register_page context manager to ignore register_page calls when importing the page modules in your tests. This is necessary because Dash raises an error if register_page is called before a Dash app is instantiated (for example, when importing page modules in a test). If you are testing a single-page Dash application, or your dash.register_page calls are in a separate file, you can omit this context manager.
Warning
SAF doesn’t support executing your callback with kwargs when using project injection.
Example#
For the tests, we are going to use a minimal SAF-based solution created with the SAF CLI saf new command.
This solution generated from the template has a simple Dash UI page, which includes a callback to execute a transaction
and set and retrieve some step fields.
@callback(
Output("result", "children"),
Input("calculate", "n_clicks"),
State("first-arg", "value"),
State("second-arg", "value"),
State("url", "pathname"),
prevent_initial_call=True,
)
def calculate(n_clicks: int, first_arg: int, second_arg: int, project: MySolution):
"""Using project injection."""
step = project.steps.first_step
step.first_arg = first_arg
step.second_arg = second_arg
step.calculate()
return step.result
To test it, use the fixture init_dashclient in your test function and run your callback. You must use the fixture project_name as a value of pathname.
from dash.testing import ignore_register_page
with ignore_register_page():
from ansys.solutions.my_solution.ui.pages.first_page import calculate
pytestmark = [pytest.mark.usefixtures("init_dashclient")]
def test_callback(project_name: str):
assert calculate(1, 1, 2, project_name) == (3.0, True) # This works!
assert calculate(n_clicks=1, first_arg=1, second_arg=2, pathname=project_name) == (3.0, True) # This does not work!
Note
Even if SAF GLOW Engine automatically injects the project when running the solution, you still need to pass the pathname argument when manually calling the callback function.
Test WebSocket event consumption on the UI#
The dash_http_client fixture sets up a Dash test client that allows you to test WebSocket event streams
emitted from transactions in your solution.
Example#
Given a Dash callback that triggers the trigger_event transaction which raises an event:
@callback(
Input("start-transaction", "n_clicks"),
State("url", "pathname"),
prevent_initial_call=True,
)
def trigger_transaction_event(n_clicks: int, project: MySolution) -> None:
if n_clicks > 0:
step = project.steps.first_step
step.trigger_event()
@transaction(self=StepSpec())
def trigger_event(self) -> None:
self.transaction.raise_event(message={"message": "testing!"}, stream_name="my-stream")
Then, the event message can be tested as follows:
from dash.testing import ignore_register_page
with ignore_register_page():
from ansys.solutions.my_solution.ui.pages import first_page
pytestmark = [pytest.mark.usefixtures("init_dashclient")]
def test_event_triggered_from_callback(dash_http_client: TestClient, project_name: str):
# Set the WebSocket URL for the event stream
ws_url = f"/events/{project_name}/steps/first-step/streams/my-stream"
with dash_http_client.websocket_connect(ws_url) as ws:
# Trigger the callback that starts the transaction
first_page.trigger_transaction_event(1, project_name)
# Receive the event from the stream
event = ws.receive_json()
assert event["message"] == "testing!"
If the transaction raises multiple events, you can receive and verify them in sequence:
@transaction(self=StepSpec())
def trigger_event(self) -> None:
self.transaction.raise_event(message={"message": "first_message"}, stream_name="my-stream")
self.transaction.raise_event(message={"message": "second_message"}, stream_name="my-stream")
from dash.testing import ignore_register_page
with ignore_register_page():
from ansys.solutions.my_solution.ui.pages import first_page
pytestmark = [pytest.mark.usefixtures("init_dashclient")]
def test_multiple_events_from_transaction(dash_http_client: TestClient, project_name: str):
# Set the WebSocket URL for the event stream
ws_url = f"/events/{project_name}/steps/first-step/streams/my-stream"
with dash_http_client.websocket_connect(ws_url) as ws:
# Trigger the callback that starts the transaction
first_page.trigger_transaction_event(1, project_name)
# Receive the first event from the stream
event_1 = ws.receive_json()
assert event_1["message"] == "first_message"
# Receive the second event from the stream
event_2 = ws.receive_json()
assert event_2["message"] == "second_message"
If the transaction raises a termination event, you can also connect to that WebSocket stream and verify the termination event.
@transaction(enable_termination_event=True, self=StepSpec())
def trigger_event(self) -> None:
self.transaction.raise_event(message={"message": "testing!"}, stream_name="my-stream")
from dash.testing import ignore_register_page
with ignore_register_page():
from ansys.solutions.my_solution.ui.pages import first_page
pytestmark = [pytest.mark.usefixtures("init_dashclient")]
def test_transaction_termination_event(dash_http_client: TestClient, project_name: str):
# Set the WebSocket URL for the termination event stream
termination_event_ws_url = f"/events/{project_name}/steps/first-step/streams/trigger-event"
with dash_http_client.websocket_connect(termination_event_ws_url) as termination_ws:
# Trigger the callback that starts the transaction
first_page.trigger_transaction_event(1, project_name)
# Receive the termination event from the trigger-event stream
termination_event = termination_ws.receive_json()
assert termination_event["status"] == "completed"
Note
The stream name for the termination event is derived from the transaction method name.
It is also possible to emulate the behavior of Dash event listeners by passing the received event data to a callback function,
putting the event data inside a dictionary with the key data:
@callback(
Output("event_message", "children"),
Input("my_ws", "event"),
prevent_initial_call=True,
)
def message(event: dict) -> str:
if message:
return f"Received message: {message['data']}"
else:
return "No message received yet."
from dash.testing import ignore_register_page
with ignore_register_page():
from ansys.solutions.my_solution.ui.pages import first_page
pytestmark = [pytest.mark.usefixtures("init_dashclient")]
def test_event_callback_emulation(dash_http_client: TestClient, project_name: str):
# Set the WebSocket URL for the event stream
ws_url = f"/events/{project_name}/steps/first-step/streams/my-stream"
with dash_http_client.websocket_connect(ws_url) as ws:
# Trigger the callback that starts the transaction
first_page.trigger_transaction_event(1, project_name)
# Receive the event from the stream
event = ws.receive_json()
# Emulate dash-extensions event listener behavior, which passes {"data": event} to the callback
assert first_page.message({"data": event}) == "Received message: {'message': 'testing!'}"