feat(web console): add support for shared web console sessions

This commit is contained in:
2026-03-05 18:12:31 +01:00
committed by wyzula_j
parent 836fedd50e
commit 5e111cfc54
10 changed files with 883 additions and 49 deletions
+1 -1
View File
@@ -2208,7 +2208,7 @@ class TestFlatToolbarActions:
"flat_status": "BECStatusBox",
"flat_progress_bar": "RingProgressBar",
"flat_terminal": "WebConsole",
"flat_bec_shell": "WebConsole",
"flat_bec_shell": "BECShell",
"flat_sbb_monitor": "SBBMonitor",
}
+338 -9
View File
@@ -1,25 +1,69 @@
from unittest import mock
import pytest
from qtpy.QtCore import Qt
from qtpy.QtGui import QHideEvent
from qtpy.QtNetwork import QAuthenticator
from bec_widgets.widgets.editors.web_console.web_console import WebConsole, _web_console_registry
from bec_widgets.widgets.editors.web_console.web_console import (
BECShell,
ConsoleMode,
WebConsole,
_web_console_registry,
)
from .client_mocks import mocked_client
@pytest.fixture
def console_widget(qtbot, mocked_client):
def mocked_server_startup():
"""Mock the web console server startup process."""
with mock.patch(
"bec_widgets.widgets.editors.web_console.web_console.subprocess"
) as mock_subprocess:
with mock.patch.object(_web_console_registry, "_wait_for_server_port"):
_web_console_registry._server_port = 12345
# Create the WebConsole widget
widget = WebConsole(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
yield mock_subprocess
def static_console(qtbot, client, unique_id: str | None = None):
"""Fixture to provide a static unique_id for WebConsole tests."""
if unique_id is None:
widget = WebConsole(client=client)
else:
widget = WebConsole(client=client, unique_id=unique_id)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
return widget
@pytest.fixture
def console_widget(qtbot, mocked_client, mocked_server_startup):
"""Create a WebConsole widget with mocked server startup."""
yield static_console(qtbot, mocked_client)
@pytest.fixture
def bec_shell_widget(qtbot, mocked_client, mocked_server_startup):
"""Create a BECShell widget with mocked server startup."""
widget = BECShell(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
@pytest.fixture
def console_widget_with_static_id(qtbot, mocked_client, mocked_server_startup):
"""Create a WebConsole widget with a static unique ID."""
yield static_console(qtbot, mocked_client, unique_id="test_console")
@pytest.fixture
def two_console_widgets_same_id(qtbot, mocked_client, mocked_server_startup):
"""Create two WebConsole widgets sharing the same unique ID."""
widget1 = static_console(qtbot, mocked_client, unique_id="shared_console")
widget2 = static_console(qtbot, mocked_client, unique_id="shared_console")
yield widget1, widget2
def test_web_console_widget_initialization(console_widget):
@@ -34,7 +78,7 @@ def test_web_console_write(console_widget):
with mock.patch.object(console_widget.page, "runJavaScript") as mock_run_js:
console_widget.write("Hello, World!")
assert mock.call("window.term.paste('Hello, World!');") in mock_run_js.mock_calls
assert mock.call('window.term.paste("Hello, World!");') in mock_run_js.mock_calls
def test_web_console_write_no_return(console_widget):
@@ -42,7 +86,7 @@ def test_web_console_write_no_return(console_widget):
with mock.patch.object(console_widget.page, "runJavaScript") as mock_run_js:
console_widget.write("Hello, World!", send_return=False)
assert mock.call("window.term.paste('Hello, World!');") in mock_run_js.mock_calls
assert mock.call('window.term.paste("Hello, World!");') in mock_run_js.mock_calls
assert mock_run_js.call_count == 1
@@ -138,6 +182,20 @@ def test_web_console_startup_command_execution(console_widget, qtbot):
assert not console_widget._startup_timer.isActive()
def test_bec_shell_startup_contains_gui_id(bec_shell_widget):
"""Test that the BEC shell startup command includes the GUI ID."""
bec_shell = bec_shell_widget
assert bec_shell._is_bec_shell
assert bec_shell._unique_id == "bec_shell"
assert bec_shell.startup_cmd == "bec --nogui"
with mock.patch.object(bec_shell.bec_dispatcher, "cli_server") as mock_cli_server:
mock_cli_server.gui_id = "test_gui_id"
assert bec_shell.startup_cmd == "bec --gui-id test_gui_id"
def test_web_console_set_readonly(console_widget):
# Test the set_readonly method
console_widget.set_readonly(True)
@@ -145,3 +203,274 @@ def test_web_console_set_readonly(console_widget):
console_widget.set_readonly(False)
assert console_widget.isEnabled()
def test_web_console_with_unique_id(console_widget_with_static_id):
"""Test creating a WebConsole with a unique_id."""
widget = console_widget_with_static_id
assert widget._unique_id == "test_console"
assert widget._unique_id in _web_console_registry._page_registry
page_info = _web_console_registry.get_page_info("test_console")
assert page_info is not None
assert page_info.owner_gui_id == widget.gui_id
assert widget.gui_id in page_info.widget_ids
def test_web_console_page_sharing(two_console_widgets_same_id):
"""Test that two widgets can share the same page using unique_id."""
widget1, widget2 = two_console_widgets_same_id
# Both should reference the same page in the registry
page_info = _web_console_registry.get_page_info("shared_console")
assert page_info is not None
assert widget1.gui_id in page_info.widget_ids
assert widget2.gui_id in page_info.widget_ids
assert widget1.page == widget2.page
def test_web_console_has_ownership(console_widget_with_static_id):
"""Test the has_ownership method."""
widget = console_widget_with_static_id
# Widget should have ownership by default
assert widget.has_ownership()
def test_web_console_yield_ownership(console_widget_with_static_id):
"""Test yielding ownership of a page."""
widget = console_widget_with_static_id
assert widget.has_ownership()
# Yield ownership
widget.yield_ownership()
# Widget should no longer have ownership
assert not widget.has_ownership()
page_info = _web_console_registry.get_page_info("test_console")
assert page_info.owner_gui_id is None
# Overlay should be shown
assert widget._mode == ConsoleMode.INACTIVE
def test_web_console_take_page_ownership(two_console_widgets_same_id):
"""Test taking ownership of a page."""
widget1, widget2 = two_console_widgets_same_id
# Widget1 should have ownership initially
assert widget1.has_ownership()
assert not widget2.has_ownership()
# Widget2 takes ownership
widget2.take_page_ownership()
# Now widget2 should have ownership
assert not widget1.has_ownership()
assert widget2.has_ownership()
assert widget2._mode == ConsoleMode.ACTIVE
assert widget1._mode == ConsoleMode.INACTIVE
def test_web_console_hide_event_yields_ownership(qtbot, console_widget_with_static_id):
"""Test that hideEvent yields ownership."""
widget = console_widget_with_static_id
assert widget.has_ownership()
# Hide the widget. Note that we cannot call widget.hide() directly
# because it doesn't trigger the hideEvent in tests as widgets are
# not visible in the test environment.
widget.hideEvent(QHideEvent())
qtbot.wait(100) # Allow event processing
# Widget should have yielded ownership
assert not widget.has_ownership()
page_info = _web_console_registry.get_page_info("test_console")
assert page_info.owner_gui_id is None
def test_web_console_show_event_takes_ownership(console_widget_with_static_id):
"""Test that showEvent takes ownership when page has no owner."""
widget = console_widget_with_static_id
# Yield ownership
widget.yield_ownership()
assert not widget.has_ownership()
# Show the widget again
widget.show()
# Widget should have reclaimed ownership
assert widget.has_ownership()
assert widget.browser.isVisible()
assert not widget.overlay.isVisible()
def test_web_console_mouse_press_takes_ownership(qtbot, two_console_widgets_same_id):
"""Test that clicking on overlay takes ownership."""
widget1, widget2 = two_console_widgets_same_id
widget1.show()
widget2.show()
# Widget1 has ownership, widget2 doesn't
assert widget1.has_ownership()
assert not widget2.has_ownership()
assert widget1.isVisible()
assert widget1._mode == ConsoleMode.ACTIVE
assert widget2._mode == ConsoleMode.INACTIVE
qtbot.mouseClick(widget2, Qt.MouseButton.LeftButton)
# Widget2 should now have ownership
assert widget2.has_ownership()
assert not widget1.has_ownership()
def test_web_console_registry_cleanup_removes_page(console_widget_with_static_id):
"""Test that the registry cleans up pages when all widgets are removed."""
widget = console_widget_with_static_id
assert widget._unique_id in _web_console_registry._page_registry
# Cleanup the widget
widget.cleanup()
# Page should be removed from registry
assert widget._unique_id not in _web_console_registry._page_registry
def test_web_console_without_unique_id_no_page_sharing(console_widget):
"""Test that widgets without unique_id don't participate in page sharing."""
widget = console_widget
# Widget should not be in the page registry
assert widget._unique_id is None
assert not widget.has_ownership() # Should return False for non-unique widgets
def test_web_console_registry_get_page_info_nonexistent(qtbot, mocked_client):
"""Test getting page info for a non-existent page."""
page_info = _web_console_registry.get_page_info("nonexistent")
assert page_info is None
def test_web_console_take_ownership_without_unique_id(console_widget):
"""Test that take_page_ownership fails gracefully without unique_id."""
widget = console_widget
# Should not crash when taking ownership without unique_id
widget.take_page_ownership()
def test_web_console_yield_ownership_without_unique_id(console_widget):
"""Test that yield_ownership fails gracefully without unique_id."""
widget = console_widget
# Should not crash when yielding ownership without unique_id
widget.yield_ownership()
def test_registry_yield_ownership_gui_id_not_in_instances():
"""Test registry yield_ownership returns False when gui_id not in instances."""
result = _web_console_registry.yield_ownership("nonexistent_gui_id")
assert result is False
def test_registry_yield_ownership_instance_is_none(console_widget_with_static_id):
"""Test registry yield_ownership returns False when instance weakref is dead."""
widget = console_widget_with_static_id
gui_id = widget.gui_id
# Store the gui_id and simulate the weakref being dead
_web_console_registry._instances[gui_id] = lambda: None
result = _web_console_registry.yield_ownership(gui_id)
assert result is False
def test_registry_yield_ownership_unique_id_none(console_widget_with_static_id):
"""Test registry yield_ownership returns False when page info's unique_id is None."""
widget = console_widget_with_static_id
gui_id = widget.gui_id
unique_id = widget._unique_id
widget._unique_id = None
result = _web_console_registry.yield_ownership(gui_id)
assert result is False
widget._unique_id = unique_id # Restore for cleanup
def test_registry_yield_ownership_unique_id_not_in_page_registry(console_widget_with_static_id):
"""Test registry yield_ownership returns False when unique_id not in page registry."""
widget = console_widget_with_static_id
gui_id = widget.gui_id
unique_id = widget._unique_id
widget._unique_id = "nonexistent_unique_id"
result = _web_console_registry.yield_ownership(gui_id)
assert result is False
widget._unique_id = unique_id # Restore for cleanup
def test_registry_owner_is_visible_page_info_none():
"""Test owner_is_visible returns False when page info doesn't exist."""
result = _web_console_registry.owner_is_visible("nonexistent_page")
assert result is False
def test_registry_owner_is_visible_no_owner(console_widget_with_static_id):
"""Test owner_is_visible returns False when page has no owner."""
widget = console_widget_with_static_id
# Yield ownership so there's no owner
widget.yield_ownership()
page_info = _web_console_registry.get_page_info(widget._unique_id)
assert page_info.owner_gui_id is None
result = _web_console_registry.owner_is_visible(widget._unique_id)
assert result is False
def test_registry_owner_is_visible_owner_ref_none(console_widget_with_static_id):
"""Test owner_is_visible returns False when owner ref doesn't exist in instances."""
widget = console_widget_with_static_id
unique_id = widget._unique_id
# Remove owner from instances dict
del _web_console_registry._instances[widget.gui_id]
result = _web_console_registry.owner_is_visible(unique_id)
assert result is False
def test_registry_owner_is_visible_owner_instance_none(console_widget_with_static_id):
"""Test owner_is_visible returns False when owner instance weakref is dead."""
widget = console_widget_with_static_id
unique_id = widget._unique_id
gui_id = widget.gui_id
# Simulate dead weakref
_web_console_registry._instances[gui_id] = lambda: None
result = _web_console_registry.owner_is_visible(unique_id)
assert result is False
def test_registry_owner_is_visible_owner_visible(console_widget_with_static_id):
"""Test owner_is_visible returns True when owner is visible."""
widget = console_widget_with_static_id
widget.show()
result = _web_console_registry.owner_is_visible(widget._unique_id)
assert result is True
def test_registry_owner_is_visible_owner_not_visible(console_widget_with_static_id):
"""Test owner_is_visible returns False when owner is not visible."""
widget = console_widget_with_static_id
widget.hide()
result = _web_console_registry.owner_is_visible(widget._unique_id)
assert result is False