From c6a71fb62af76db6bceb894873e7db74001241ec Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Sat, 2 May 2026 07:43:29 +0300 Subject: [PATCH 01/48] remove benchmark workflow and container comp. tests pages --- .../pages/benchmark/workflow_test_page.py | 37 ------------------- .../container/compatibility_test_page.py | 25 ------------- 2 files changed, 62 deletions(-) delete mode 100644 cli/medperf/web_ui/tests/pages/benchmark/workflow_test_page.py delete mode 100644 cli/medperf/web_ui/tests/pages/container/compatibility_test_page.py diff --git a/cli/medperf/web_ui/tests/pages/benchmark/workflow_test_page.py b/cli/medperf/web_ui/tests/pages/benchmark/workflow_test_page.py deleted file mode 100644 index 77b310eff..000000000 --- a/cli/medperf/web_ui/tests/pages/benchmark/workflow_test_page.py +++ /dev/null @@ -1,37 +0,0 @@ -# This is not used anymore - -from selenium.webdriver.common.by import By -from ..base_page import BasePage - - -class WorkflowTestPage(BasePage): - BMK_REG_BTN = (By.CSS_SELECTOR, '[data-testid="reg-bmk-btn"]') - WF_BTN = (By.CSS_SELECTOR, 'a[href="/benchmarks/register/workflow_test"]') - DATA_PREP_CONFIG = (By.ID, "data-preparation") - DATA_PREP_PARAMETERS = (By.ID, "data-preparation-parameters") - DATA_PREP_ADDITIONAL = (By.ID, "data-preparation-additional") - MODEL_CONFIG = (By.ID, "model-path") - MODEL_PARAMETERS = (By.ID, "model-parameters-path") - MODEL_ADDITIONAL = (By.ID, "model-additional-path") - EVALUATOR_CONFIG = (By.ID, "evaluator-path") - EVALUATOR_PARAMETERS = (By.ID, "evaluator-parameters-path") - EVALUATOR_ADDITIONAL = (By.ID, "evaluator-additional-path") - DATA = (By.ID, "data-path") - LABELS = (By.ID, "labels-path") - RUN_TEST_BTN = (By.ID, "run-workflow-test-btn") - CONTINUE_BTN = (By.CSS_SELECTOR, "button.btn-success") - - def run_test(self, data_prep, ref_model, evaluator, data_path, labels_path): - self.type(self.DATA_PREP_CONFIG, data_prep.config) - self.type(self.DATA_PREP_PARAMETERS, data_prep.parameters) - self.type(self.DATA_PREP_ADDITIONAL, data_prep.additional_local) - self.type(self.MODEL_CONFIG, ref_model.config) - self.type(self.MODEL_PARAMETERS, ref_model.parameters) - self.type(self.MODEL_ADDITIONAL, ref_model.additional_local) - self.type(self.EVALUATOR_CONFIG, evaluator.config) - self.type(self.EVALUATOR_PARAMETERS, evaluator.parameters) - self.type(self.EVALUATOR_ADDITIONAL, evaluator.additional_local) - self.type(self.DATA, data_path) - self.type(self.LABELS, labels_path) - - self.click(self.RUN_TEST_BTN) diff --git a/cli/medperf/web_ui/tests/pages/container/compatibility_test_page.py b/cli/medperf/web_ui/tests/pages/container/compatibility_test_page.py deleted file mode 100644 index 8ed2bbb79..000000000 --- a/cli/medperf/web_ui/tests/pages/container/compatibility_test_page.py +++ /dev/null @@ -1,25 +0,0 @@ -# This is not used anymore - -from selenium.webdriver.common.by import By -from ..base_page import BasePage - - -class CompatibilityTestPage(BasePage): - REG_CONT_BTN = (By.CSS_SELECTOR, '[data-testid="reg-cont-btn"]') - COMP_BTN = (By.CSS_SELECTOR, 'a[href="/containers/register/compatibility_test"]') - BENCHMARK = (By.ID, "benchmark") - MODEL_CONFIG = (By.ID, "container-file") - MODEL_PARAMETERS = (By.ID, "parameters-file") - MODEL_ADDITIONAL = (By.ID, "additional-file") - RUN_TEST_BTN = (By.ID, "run-comp-test-btn") - CONTINUE_BTN = (By.CSS_SELECTOR, "button.btn-success") - NOT_ENCRYPTED = (By.ID, "without-encryption") - - def run_test(self, benchmark, model): - self.select_by_text(self.BENCHMARK, benchmark) - self.type(self.MODEL_CONFIG, model.config) - self.type(self.MODEL_PARAMETERS, model.parameters) - self.type(self.MODEL_ADDITIONAL, model.additional_local) - self.click(self.NOT_ENCRYPTED) - - self.click(self.RUN_TEST_BTN) From 209434cefa89473387a3aec3a2e15af7594f37ec Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Sat, 2 May 2026 07:44:53 +0300 Subject: [PATCH 02/48] use get_task_id as a helper function to be mocked during testing --- cli/medperf/web_ui/events.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/medperf/web_ui/events.py b/cli/medperf/web_ui/events.py index 58a1d987c..5a28baebe 100644 --- a/cli/medperf/web_ui/events.py +++ b/cli/medperf/web_ui/events.py @@ -30,8 +30,7 @@ def delete_notification( config.ui.delete_notification(notification_id) -@router.get("/current_task", response_class=JSONResponse) -def get_task_id(request: Request, current_user: bool = Depends(check_user_api)): +def _get_task_id() -> dict: start_time = time.monotonic() while not config.ui.task_id: time.sleep(0.1) @@ -40,6 +39,11 @@ def get_task_id(request: Request, current_user: bool = Depends(check_user_api)): return {"task_id": config.ui.task_id} +@router.get("/current_task", response_class=JSONResponse) +def get_task_id(request: Request, current_user: bool = Depends(check_user_api)): + return _get_task_id() + + def process_event(request: Request, event: EventBase): if request.app.state.task.running and event.task_id == request.app.state.task.id: request.app.state.task.add_log(event) From a69a708ebd913f630d08bfb329159c8de32360e0 Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Mon, 4 May 2026 11:09:27 +0300 Subject: [PATCH 03/48] add `data-testid` attributes for aggregator pages - unit tests --- .../aggregators/aggregator_detail.html | 36 +++++++++---------- .../aggregators/register_aggregator.html | 24 ++++++------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/cli/medperf/web_ui/templates/aggregators/aggregator_detail.html b/cli/medperf/web_ui/templates/aggregators/aggregator_detail.html index 40b13a87c..1e46d5bfe 100644 --- a/cli/medperf/web_ui/templates/aggregators/aggregator_detail.html +++ b/cli/medperf/web_ui/templates/aggregators/aggregator_detail.html @@ -11,58 +11,58 @@
-

{{ entity.name }}

+

{{ entity.name }}

-

+

Details

-

Aggregator ID

-

{{ entity.id }}

+

Aggregator ID

+

{{ entity.id }}

-

Owner

-

+

Owner

+

{% if owner %} You {% else %}{{ entity.owner }}{% endif %}

-

Address

-

{{ entity.address }}

+

Address

+

{{ entity.address }}

-

Port

-

{{ entity.port }}

+

Port

+

{{ entity.port }}

-

Admin Port

-

{{ entity.admin_port }}

+

Admin Port

+

{{ entity.admin_port }}

-

Created

- +

Created

+
-

Modified

- +

Modified

+
-

Training experiments

+

Training experiments

{% if experiments_using_aggregator %} @@ -84,7 +84,7 @@

Training experiments
-

Actions

+

Actions

diff --git a/cli/medperf/web_ui/templates/aggregators/register_aggregator.html b/cli/medperf/web_ui/templates/aggregators/register_aggregator.html index 99be7bbb6..0c95e6553 100644 --- a/cli/medperf/web_ui/templates/aggregators/register_aggregator.html +++ b/cli/medperf/web_ui/templates/aggregators/register_aggregator.html @@ -10,42 +10,42 @@ {% endif %}
-

Register New Aggregator

+

Register New Aggregator

Register a new aggregator with address, port, and aggregation container.

-
+
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
From 414477fe71ae074a4e9b9b71f9b14483282bd2a9 Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Mon, 4 May 2026 11:10:25 +0300 Subject: [PATCH 04/48] add `data-testid` attributes for benchmark pages - unit tests --- .../templates/benchmark/benchmark_detail.html | 60 +++++++++---------- .../benchmark/register_benchmark.html | 39 +++++++----- 2 files changed, 54 insertions(+), 45 deletions(-) diff --git a/cli/medperf/web_ui/templates/benchmark/benchmark_detail.html b/cli/medperf/web_ui/templates/benchmark/benchmark_detail.html index 91b7ec345..fa421b08d 100644 --- a/cli/medperf/web_ui/templates/benchmark/benchmark_detail.html +++ b/cli/medperf/web_ui/templates/benchmark/benchmark_detail.html @@ -12,31 +12,31 @@
-

{{ entity.name }}

+

{{ entity.name }}

-

+

Details

- {% if entity.state == 'OPERATION' %}OPERATIONAL{% else %}{{ entity.state }}{% endif %} - {{ 'VALID' if entity.is_valid else 'INVALID' }} + {% if entity.state == 'OPERATION' %}OPERATIONAL{% else %}{{ entity.state }}{% endif %} + {{ 'VALID' if entity.is_valid else 'INVALID' }}
-

Benchmark ID

-

{{ entity.id }}

+

Benchmark ID

+

{{ entity.id }}

-

Owner

-

+

Owner

+

{% if current_user_is_benchmark_owner %} You {% else %}{{ entity.owner }}{% endif %}

@@ -44,50 +44,50 @@

-

Description

-

{{ entity.description }}

+

Description

+

{{ entity.description }}

-

Documentation

+

Documentation

{% if entity.docs_url %} - {{ entity.docs_url }} + {{ entity.docs_url }} {% else %} - Not Available + Not Available {% endif %}
-

Reference Dataset Tarball

- Click to Download the File +

Reference Dataset Tarball

+ Click to Download the File
-
-

Data Preparation Container

+
+

Data Preparation Container

{{ container_macros.container_link(data_preparation_container) }}
-
-

Reference Model

+
+

Reference Model

{{ model_macros.model_link(reference_model) }}
-
-

Metrics Container

+
+

Metrics Container

{{ container_macros.container_link(metrics_container) }}
-

Created

- +

Created

+
-

Modified

- +

Modified

+
{% if current_user_is_benchmark_owner %}
-

+

Association Policy

@@ -102,7 +102,7 @@
@@ -116,7 +116,7 @@
@@ -146,7 +146,7 @@
@@ -163,7 +163,7 @@
diff --git a/cli/medperf/web_ui/templates/benchmark/register_benchmark.html b/cli/medperf/web_ui/templates/benchmark/register_benchmark.html index 1a6cf139a..974b226d0 100644 --- a/cli/medperf/web_ui/templates/benchmark/register_benchmark.html +++ b/cli/medperf/web_ui/templates/benchmark/register_benchmark.html @@ -8,42 +8,51 @@ {% set benchmark_register_running = (task_running and request.app.state.task.name == "register_benchmark") %} {% if benchmark_register_running %} {% set form_data = request.app.state.task.formData %} +{% set skip_compatibility = form_data.get("skip_compatibility_tests", "false") %} +{% set skip_dataprep = form_data.get("skip_data_preparation_step", "false") %} +{% elif task_running %} +{# Another task is running: leave radios unchecked so the form is clearly not mid-register_benchmark. #} +{% set skip_compatibility = none %} +{% set skip_dataprep = none %} +{% else %} +{% set skip_compatibility = "false" %} +{% set skip_dataprep = "false" %} {% endif %}
-

Register a New Benchmark

+

Register a New Benchmark

- - + +
- +
- +
- +
- +
- +
- +
- - + +
- + {% if reference_models | length > 0 %} @@ -69,7 +78,7 @@

Metrics Container + - + +

- +
- +
- +
- +
- +
- +
{% if ui_mode == request.app.state.EVALUATION_MODE %} From 42fb2b63df8d1201e5e797f29f59dc316d188008 Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Mon, 4 May 2026 11:11:34 +0300 Subject: [PATCH 06/48] add `data-testid` attributes for dataset pages - unit tests --- .../templates/dataset/dataset_detail.html | 40 +++++++++---------- .../templates/dataset/register_dataset.html | 8 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/cli/medperf/web_ui/templates/dataset/dataset_detail.html b/cli/medperf/web_ui/templates/dataset/dataset_detail.html index 92cd1a2b4..35854a983 100644 --- a/cli/medperf/web_ui/templates/dataset/dataset_detail.html +++ b/cli/medperf/web_ui/templates/dataset/dataset_detail.html @@ -13,7 +13,7 @@
-

{{ dataset.name }}

+

{{ dataset.name }}

@@ -24,23 +24,23 @@

{{ dataset.nam
-

+

Details

- {% if dataset.state == 'OPERATION' %}OPERATIONAL{% else %}{{ dataset.state }}{% endif %} - {% if dataset.is_valid %}VALID{% else %}INVALID{% endif %} + {% if dataset.state == 'OPERATION' %}OPERATIONAL{% else %}{{ dataset.state }}{% endif %} + {% if dataset.is_valid %}VALID{% else %}INVALID{% endif %}
-

Dataset ID

-

{{ dataset.id }}

+

Dataset ID

+

{{ dataset.id }}

-

Owner

-

+

Owner

+

{% if is_owner %} You {% else %}{{ dataset.owner }}{% endif %}

@@ -55,34 +55,34 @@

Location

{{ dataset.location }}

-
-

Data Preparation Container

+
+

Data Preparation Container

{{ container_macros.container_link(prep_cube) }}
{% if dataset.generated_metadata %}
-

Statistics

- +

Statistics

+
{% endif %} {% if report_exists %}
-

Report

- +

Report

+
{% endif %}
-

Is Prepared

-

{{ dataset_is_prepared }}

+

Is Prepared

+

{{ dataset_is_prepared }}

-

Created

- +

Created

+
-

Modified

- +

Modified

+
diff --git a/cli/medperf/web_ui/templates/dataset/register_dataset.html b/cli/medperf/web_ui/templates/dataset/register_dataset.html index bf2661ad3..5ebcf90e0 100644 --- a/cli/medperf/web_ui/templates/dataset/register_dataset.html +++ b/cli/medperf/web_ui/templates/dataset/register_dataset.html @@ -15,8 +15,8 @@

{% if request.app.state.ui_mode == request.app.state.EVALUATION_MODE %} -
- +
+ From 951acaff04518deea0b1d97c7f204d6df7dba081 Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Mon, 4 May 2026 11:12:54 +0300 Subject: [PATCH 07/48] add `data-testid` attributes for macros - update with task_running - unit tests --- .../macros/association_card_macros.html | 22 +++++++++++++++---- .../templates/macros/container_macros.html | 10 ++++----- .../macros/model_detail_util_macro.html | 12 +++++----- .../web_ui/templates/macros/model_macros.html | 10 ++++----- .../templates/macros/result_card_macros.html | 2 +- .../web_ui/templates/model/model_detail.html | 2 +- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/cli/medperf/web_ui/templates/macros/association_card_macros.html b/cli/medperf/web_ui/templates/macros/association_card_macros.html index 27b542a81..6e7b79721 100644 --- a/cli/medperf/web_ui/templates/macros/association_card_macros.html +++ b/cli/medperf/web_ui/templates/macros/association_card_macros.html @@ -1,5 +1,5 @@ -{% macro association_card(assoc, entity, entity_type) %} -
+{% macro association_card(assoc, entity, entity_type, task_running) %} +

{{ entity.name }}

@@ -12,13 +12,27 @@

{% if entity_type == "dataset" %}{% endif %} {% if entity_type == "container" or entity_type == "model" %}{% endif %} - +
{% if entity_type == "dataset" %}{% endif %} {% if entity_type == "container" or entity_type == "model" %}{% endif %} - +
{% endif %} diff --git a/cli/medperf/web_ui/templates/macros/container_macros.html b/cli/medperf/web_ui/templates/macros/container_macros.html index 5c2553ec3..b44089d22 100644 --- a/cli/medperf/web_ui/templates/macros/container_macros.html +++ b/cli/medperf/web_ui/templates/macros/container_macros.html @@ -1,13 +1,13 @@ {% macro container_link(container) %} - + - {{ container.name }} + {{ container.name }} - + {% if container.state == 'OPERATION' %} - + {% else %} - + {% endif %} {% endmacro %} diff --git a/cli/medperf/web_ui/templates/macros/model_detail_util_macro.html b/cli/medperf/web_ui/templates/macros/model_detail_util_macro.html index 4688037b7..608a57f54 100644 --- a/cli/medperf/web_ui/templates/macros/model_detail_util_macro.html +++ b/cli/medperf/web_ui/templates/macros/model_detail_util_macro.html @@ -2,16 +2,16 @@ {% macro container_detail_util(container) %}
-

Container Manifest

- +

Container Manifest

+
-

Parameters

- {% if container.parameters_config is none %}Not Available{% else %}{% endif %} +

Parameters

+ {% if container.parameters_config is none %}Not Available{% else %}{% endif %}
-

Additional Files

- {% if container.additional_files_tarball_url %}Click to Download the File{% else %}Not Available{% endif %} +

Additional Files

+ {% if container.additional_files_tarball_url %}Click to Download the File{% else %}Not Available{% endif %}
{% endmacro %} diff --git a/cli/medperf/web_ui/templates/macros/model_macros.html b/cli/medperf/web_ui/templates/macros/model_macros.html index 8b1dcfca6..7fe544326 100644 --- a/cli/medperf/web_ui/templates/macros/model_macros.html +++ b/cli/medperf/web_ui/templates/macros/model_macros.html @@ -1,15 +1,15 @@ {# ./cli/medperf/web_ui/templates/macros/model_macros.html #} {% macro model_link(model) %} - + - {{ model.name }} + {{ model.name }} - + {% if model.state == 'OPERATION' %} - + {% else %} - + {% endif %} {% endmacro %} diff --git a/cli/medperf/web_ui/templates/macros/result_card_macros.html b/cli/medperf/web_ui/templates/macros/result_card_macros.html index 6887d3c69..05f45ee16 100644 --- a/cli/medperf/web_ui/templates/macros/result_card_macros.html +++ b/cli/medperf/web_ui/templates/macros/result_card_macros.html @@ -1,5 +1,5 @@ {% macro result_card(result) %} -
+

{{ result.name }}

{% if result.finalized %} diff --git a/cli/medperf/web_ui/templates/model/model_detail.html b/cli/medperf/web_ui/templates/model/model_detail.html index 6d2c02cea..d24667ee8 100644 --- a/cli/medperf/web_ui/templates/model/model_detail.html +++ b/cli/medperf/web_ui/templates/model/model_detail.html @@ -121,7 +121,7 @@

Confidenti

Associated Benchmarks

{% for assoc in benchmarks_associations.values() %} - {{ association_card_macros.association_card(assoc, benchmarks[assoc.benchmark], "benchmark") }} + {{ association_card_macros.association_card(assoc, benchmarks[assoc.benchmark], "benchmark", task_running) }} {% endfor %}
{% endif %} From a0df3db307dc281f27458d567150aafbac059c9b Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Mon, 4 May 2026 11:13:13 +0300 Subject: [PATCH 08/48] add `data-testid` attributes for navbar - unit tests --- .../web_ui/templates/partials/navbar.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/medperf/web_ui/templates/partials/navbar.html b/cli/medperf/web_ui/templates/partials/navbar.html index 773c97dca..eaba285f7 100644 --- a/cli/medperf/web_ui/templates/partials/navbar.html +++ b/cli/medperf/web_ui/templates/partials/navbar.html @@ -18,19 +18,19 @@ MedPerf Logo

From 9b32998c7c95bd49e8bce1c4990636eee86e597c Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Mon, 4 May 2026 11:13:27 +0300 Subject: [PATCH 09/48] add `data-testid` attributes for training experiment pages - unit tests --- .../register_training_experiment.html | 16 +++---- .../training/training_experiment_detail.html | 48 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/cli/medperf/web_ui/templates/training/register_training_experiment.html b/cli/medperf/web_ui/templates/training/register_training_experiment.html index 2be990418..e73f31b2e 100644 --- a/cli/medperf/web_ui/templates/training/register_training_experiment.html +++ b/cli/medperf/web_ui/templates/training/register_training_experiment.html @@ -10,24 +10,24 @@ {% set form_data = request.app.state.task.formData %} {% endif %}
-

Register New Training Experiment

+

Register New Training Experiment

Register a new training experiment with data preparation, FL, and optional FL admin containers.

- +
- +
- +
- +
@@ -38,7 +38,7 @@

Data Preparation Container +
- +

- +
element. + TRAINING_EXP_SELECT = (By.ID, "training-exp-id") + PUBLISH_ON_INPUT = (By.ID, "publish-on-input") def get_server_certificate(self): self.click(self.GET_CERT_SUBMIT) - def run_aggregator_for_experiment(self, experiment_name_substring: str): - select_el = self.find(self.TRAINING_EXP_SELECT) - self.ensure_element_ready(select_el) - for opt in select_el.find_elements(By.TAG_NAME, "option"): - if experiment_name_substring in opt.text: - opt.click() - break - else: - raise AssertionError( - f"No training experiment option matching {experiment_name_substring!r}" - ) + def run_aggregator_for_experiment(self, experiment_name: str, publish_on: str = ""): + self.select_searchable_entity(self.TRAINING_EXP_SELECT, experiment_name) + if publish_on: + el = self.find(self.PUBLISH_ON_INPUT) + self.ensure_element_ready(el) + el.clear() + el.send_keys(publish_on) self.click(self.RUN_BTN) diff --git a/cli/medperf/web_ui/tests/pages/aggregator/ui_page.py b/cli/medperf/web_ui/tests/pages/aggregator/ui_page.py index 94f7b7f67..8e166a75d 100644 --- a/cli/medperf/web_ui/tests/pages/aggregator/ui_page.py +++ b/cli/medperf/web_ui/tests/pages/aggregator/ui_page.py @@ -10,10 +10,15 @@ class AggregatorsPage(BasePage): MINE_LABEL = ( By.XPATH, - "//label[.//input[@id='switch']]//span[contains(@class,'font-semibold')]", + "//label[.//input[@id='switch']]//span[contains(@class,'font-medium')]", ) MINE_INPUT = (By.ID, "switch") + SEARCH_INPUT = (By.ID, "listing-search") + SEARCH_BTN = (By.ID, "listing-search-btn") + ORDERING_SELECT = (By.ID, "ordering") + PAGE_SIZE_SELECT = (By.ID, "page-size") + CARDS_CONTAINER = ( By.CSS_SELECTOR, 'div[data-testid="cards-container"] div.medperf-card', @@ -32,10 +37,23 @@ def toggle_mine(self): self.driver.execute_script("arguments[0].click();", el) def is_mine(self): - return "?mine_only=true" in self.current_url + return "mine_only=true" in self.current_url def not_mine(self): - return "?mine_only=true" not in self.current_url + return "mine_only=true" not in self.current_url + + def search(self, query): + self.type(self.SEARCH_INPUT, query) + self.click(self.SEARCH_BTN) + + def set_ordering(self, option_text): + self.select_by_text(self.ORDERING_SELECT, option_text) + + def set_page_size(self, size): + self.select_by_text(self.PAGE_SIZE_SELECT, str(size)) + + def page_link(self, n): + return (By.CSS_SELECTOR, f'a.page-link[data-page="{n}"]') def open_aggregator_by_name(self, name: str): link = self.find( diff --git a/cli/medperf/web_ui/tests/unit/training/test_aggregator_details_page.py b/cli/medperf/web_ui/tests/unit/training/test_aggregator_details_page.py index 36217d4e0..113b322ab 100644 --- a/cli/medperf/web_ui/tests/unit/training/test_aggregator_details_page.py +++ b/cli/medperf/web_ui/tests/unit/training/test_aggregator_details_page.py @@ -1,6 +1,6 @@ import datetime from types import SimpleNamespace -from unittest.mock import MagicMock +from unittest.mock import ANY, MagicMock from medperf.tests.mocks.training_exp import TestTrainingExp import pytest @@ -8,13 +8,31 @@ from medperf.web_ui.tests import config as tests_config from medperf.web_ui.tests.pages.aggregator.details_page import AggregatorDetailsPage -from medperf.web_ui.tests.unit.helpers import switch_to_ui_mode +from medperf.web_ui.tests.unit.helpers import switch_to_ui_mode, stub_event_generator +import medperf.web_ui.events as events_module BASE_URL = tests_config.BASE_URL +PATCH_ROUTE = "medperf.web_ui.aggregators.routes.{}" TEST_TRAINING_EXPS = [TestTrainingExp(id=77, name="tr-77")] +def _make_agg(**overrides): + defaults = dict( + id=9, + name="agg-9", + owner=1, + address="127.0.0.1", + port=7000, + admin_port=7001, + created_at=datetime.datetime(2026, 1, 1), + modified_at=datetime.datetime(2026, 1, 2), + get_training_experiments=lambda: [], + ) + defaults.update(overrides) + return SimpleNamespace(**defaults) + + @pytest.fixture(autouse=True) def patch_login(mocker): mocker.patch( @@ -36,6 +54,26 @@ def page(driver): return AggregatorDetailsPage(driver) +@pytest.fixture() +def patch_common(mocker, ui): + init = mocker.patch(PATCH_ROUTE.format("initialize_state_task")) + reset = mocker.patch(PATCH_ROUTE.format("reset_state_task")) + ui.add_notification = mocker.Mock() + notifs = ui.add_notification + return (init, reset, notifs) + + +@pytest.fixture() +def patch_task_events(mocker, ui): + spy_event_gen = mocker.patch.object( + events_module, "event_generator", side_effect=stub_event_generator + ) + spy_task_id = mocker.spy(events_module, "_get_task_id") + ui.end_task = mocker.Mock() + ui.task_id = "test-id" + return (spy_event_gen, spy_task_id) + + def test_aggregator_details_page_common_content(page, mocker): agg = SimpleNamespace( id=9, @@ -46,7 +84,7 @@ def test_aggregator_details_page_common_content(page, mocker): admin_port=7001, created_at=datetime.datetime(2026, 1, 1), modified_at=datetime.datetime(2026, 1, 2), - get_training_experiments=lambda: [TEST_TRAINING_EXPS], + get_training_experiments=lambda: TEST_TRAINING_EXPS, ) mocker.patch("medperf.entities.aggregator.Aggregator.get", return_value=agg) mocker.patch("medperf.entities.ca.CA.get", return_value=MagicMock(id=1)) @@ -92,3 +130,181 @@ def test_aggregator_details_page_non_owner_content(page, mocker): with pytest.raises(NoSuchElementException): page.driver.find_element(*page.ACTIONS_HEADING) + + +def test_aggregator_details_get_server_certificate_succeed( + page, mocker, ui, patch_common, patch_task_events +): + agg = _make_agg() + mocker.patch("medperf.entities.aggregator.Aggregator.get", return_value=agg) + mocker.patch("medperf.web_ui.aggregators.routes.os.path.exists", return_value=False) + + spy_init, spy_reset, spy_notifs = patch_common + spy_event_gen, spy_task_id = patch_task_events + spy_get_cert = mocker.patch(PATCH_ROUTE.format("GetServerCertificate.run")) + + switch_to_ui_mode(page, "training") + page.open(BASE_URL.format("/aggregators/ui/display/9")) + + confirm_modal = page.find(page.PAGE_MODAL) + popup_modal = page.find(page.PAGE_MODAL) + + page.get_server_certificate() + page.wait_for_visibility_element(confirm_modal) + + assert ( + page.get_text(page.CONFIRM_TEXT) + == "Are you sure you want to get the server certificate for this aggregator?" + ) + + page.confirm_run_task() + page.wait_for_visibility_element(popup_modal) + + assert ( + page.get_text(page.PAGE_MODAL_TITLE) == "Server Certificate Retrieved Successfully" + ) + + page.wait_for_staleness_element(popup_modal) + + spy_init.assert_called_once_with(ANY, task_name="aggregator_get_server_cert") + spy_get_cert.assert_called_once_with(aggregator_id=9) + spy_reset.assert_called_once() + spy_notifs.assert_called_once() + ui.end_task.assert_called_once() + spy_event_gen.assert_called_once_with(ANY, False) + spy_task_id.assert_called_once() + + +def test_aggregator_details_get_server_certificate_fails( + page, mocker, ui, patch_common, patch_task_events +): + error_msg = "Get server certificate test failed" + agg = _make_agg() + mocker.patch("medperf.entities.aggregator.Aggregator.get", return_value=agg) + mocker.patch("medperf.web_ui.aggregators.routes.os.path.exists", return_value=False) + + spy_init, spy_reset, spy_notifs = patch_common + spy_event_gen, spy_task_id = patch_task_events + spy_get_cert = mocker.patch( + PATCH_ROUTE.format("GetServerCertificate.run"), side_effect=Exception(error_msg) + ) + + switch_to_ui_mode(page, "training") + page.open(BASE_URL.format("/aggregators/ui/display/9")) + + confirm_modal = page.find(page.PAGE_MODAL) + error_modal = page.find(page.PAGE_MODAL) + + page.get_server_certificate() + page.wait_for_visibility_element(confirm_modal) + page.confirm_run_task() + page.wait_for_visibility_element(error_modal) + page.wait_for_presence_selector(page.ERROR_RELOAD) + + assert page.get_text(page.PAGE_MODAL_TITLE) == "Failed to Get Server Certificate" + assert error_msg in page.get_text(page.ERROR_TEXT) + + hide_btn = error_modal.find_element(*page.ERROR_HIDE) + page.ensure_element_ready(hide_btn) + hide_btn.click() + page.wait_for_invisibility_element(error_modal) + + spy_init.assert_called_once_with(ANY, task_name="aggregator_get_server_cert") + spy_get_cert.assert_called_once_with(aggregator_id=9) + spy_reset.assert_called_once() + spy_notifs.assert_called_once() + ui.end_task.assert_called_once() + spy_event_gen.assert_called_once_with(ANY, False) + spy_task_id.assert_called_once() + + +def test_aggregator_details_run_aggregator_succeed( + page, mocker, ui, patch_common, patch_task_events +): + exp = TestTrainingExp(id=77, name="tr-77") + agg = _make_agg(get_training_experiments=lambda: [exp]) + mocker.patch("medperf.entities.aggregator.Aggregator.get", return_value=agg) + mocker.patch("medperf.entities.ca.CA.get", return_value=MagicMock(id=1)) + mocker.patch("medperf.web_ui.aggregators.routes.os.path.exists", return_value=True) + mocker.patch("medperf.entities.training_exp.TrainingExp.all", return_value=[exp]) + + spy_init, spy_reset, spy_notifs = patch_common + spy_event_gen, spy_task_id = patch_task_events + spy_run = mocker.patch(PATCH_ROUTE.format("StartAggregator.run")) + + switch_to_ui_mode(page, "training") + page.open(BASE_URL.format("/aggregators/ui/display/9")) + + confirm_modal = page.find(page.PAGE_MODAL) + popup_modal = page.find(page.PAGE_MODAL) + + page.run_aggregator_for_experiment("tr-77", publish_on="0.0.0.0") + page.wait_for_visibility_element(confirm_modal) + + assert ( + page.get_text(page.CONFIRM_TEXT) + == "Are you sure you want to run the aggregator for the selected training experiment?" + ) + + page.confirm_run_task() + page.wait_for_visibility_element(popup_modal) + + assert page.get_text(page.PAGE_MODAL_TITLE) == "Aggregator Ran Successfully" + + page.wait_for_staleness_element(popup_modal) + + spy_init.assert_called_once_with(ANY, task_name="start_aggregator") + spy_run.assert_called_once_with(training_exp_id=77, publish_on="0.0.0.0") + spy_reset.assert_called_once() + spy_notifs.assert_called_once() + ui.end_task.assert_called_once() + spy_event_gen.assert_called_once_with(ANY, False) + spy_task_id.assert_called_once() + + +def test_aggregator_details_run_aggregator_fails( + page, mocker, ui, patch_common, patch_task_events +): + error_msg = "Run aggregator test failed" + exp = TestTrainingExp(id=77, name="tr-77") + agg = _make_agg(get_training_experiments=lambda: [exp]) + mocker.patch("medperf.entities.aggregator.Aggregator.get", return_value=agg) + mocker.patch("medperf.entities.ca.CA.get", return_value=MagicMock(id=1)) + mocker.patch("medperf.web_ui.aggregators.routes.os.path.exists", return_value=True) + mocker.patch("medperf.entities.training_exp.TrainingExp.all", return_value=[exp]) + + spy_init, spy_reset, spy_notifs = patch_common + spy_event_gen, spy_task_id = patch_task_events + spy_run = mocker.patch( + PATCH_ROUTE.format("StartAggregator.run"), side_effect=Exception(error_msg) + ) + + switch_to_ui_mode(page, "training") + page.open(BASE_URL.format("/aggregators/ui/display/9")) + + confirm_modal = page.find(page.PAGE_MODAL) + error_modal = page.find(page.PAGE_MODAL) + + page.run_aggregator_for_experiment("tr-77", publish_on="0.0.0.0") + page.wait_for_visibility_element(confirm_modal) + page.confirm_run_task() + page.wait_for_visibility_element(error_modal) + page.wait_for_presence_selector(page.ERROR_RELOAD) + + assert page.get_text(page.PAGE_MODAL_TITLE) == ( + "Something went wrong while running the aggregator" + ) + assert error_msg in page.get_text(page.ERROR_TEXT) + + hide_btn = error_modal.find_element(*page.ERROR_HIDE) + page.ensure_element_ready(hide_btn) + hide_btn.click() + page.wait_for_invisibility_element(error_modal) + + spy_init.assert_called_once_with(ANY, task_name="start_aggregator") + spy_run.assert_called_once_with(training_exp_id=77, publish_on="0.0.0.0") + spy_reset.assert_called_once() + spy_notifs.assert_called_once() + ui.end_task.assert_called_once() + spy_event_gen.assert_called_once_with(ANY, False) + spy_task_id.assert_called_once() diff --git a/cli/medperf/web_ui/tests/unit/training/test_aggregators_page.py b/cli/medperf/web_ui/tests/unit/training/test_aggregators_page.py index 066fd55eb..508f03f29 100644 --- a/cli/medperf/web_ui/tests/unit/training/test_aggregators_page.py +++ b/cli/medperf/web_ui/tests/unit/training/test_aggregators_page.py @@ -9,8 +9,10 @@ BASE_URL = tests_config.BASE_URL PATCH_GET_AGGS = "medperf.entities.aggregator.Aggregator.all" +PATCH_GET_AGGS_COUNT = "medperf.entities.aggregator.Aggregator.get_count" PATCH_GET_USER_ID = "medperf.web_ui.aggregators.routes.get_medperf_user_data" USER_ID = 1 +PAGINATION = {"limit": 9, "offset": 0, "ordering": "-created_at"} class _TestAggregator: @@ -49,18 +51,19 @@ def page(driver): def test_empty_aggregators_ui_page_content(page, mocker): - filters = {"owner": USER_ID} + filters = {"owner": USER_ID, **PAGINATION} mocker.patch(PATCH_GET_USER_ID, return_value={"id": USER_ID}) + mocker.patch(PATCH_GET_AGGS_COUNT, return_value=0) spy_aggs = mocker.patch(PATCH_GET_AGGS, return_value=[]) switch_to_ui_mode(page, "training") page.open(BASE_URL.format("/aggregators/ui")) - spy_aggs.assert_called_with(filters={}) + spy_aggs.assert_called_with(filters=PAGINATION) assert page.get_text(page.REG_AGG_BTN) == "Register New Aggregator" assert page.get_text(page.HEADER) == "Aggregators" - assert page.get_text(page.MINE_LABEL) == "Show only my aggregators" - assert page.get_text(page.NO_AGGREGATORS) == "No aggregators yet" + assert page.get_text(page.MINE_LABEL) == "Mine only" + assert page.get_text(page.NO_AGGREGATORS) == "No aggregators found" assert page.get_attribute(page.MINE_INPUT, "data-entity-name") == "aggregators" old_url = page.current_url @@ -73,11 +76,12 @@ def test_empty_aggregators_ui_page_content(page, mocker): page.toggle_mine() page.wait_for_url_change(old_url) assert page.not_mine() - spy_aggs.assert_called_with(filters={}) + spy_aggs.assert_called_with(filters=PAGINATION) def test_aggregators_ui_page_content(page, mocker): mocker.patch(PATCH_GET_USER_ID, return_value={"id": USER_ID}) + mocker.patch(PATCH_GET_AGGS_COUNT, return_value=len(TEST_AGGREGATORS)) mocker.patch(PATCH_GET_AGGS, return_value=list(TEST_AGGREGATORS.values())) switch_to_ui_mode(page, "training") @@ -113,5 +117,47 @@ def test_aggregators_ui_page_content(page, mocker): assert page.get_text(page.REG_AGG_BTN) == "Register New Aggregator" assert page.get_text(page.HEADER) == "Aggregators" - assert page.get_text(page.MINE_LABEL) == "Show only my aggregators" + assert page.get_text(page.MINE_LABEL) == "Mine only" assert page.get_attribute(page.MINE_INPUT, "data-entity-name") == "aggregators" + + +def test_aggregators_ui_page_search_sort_pagination(page, mocker): + mocker.patch(PATCH_GET_USER_ID, return_value={"id": USER_ID}) + mocker.patch(PATCH_GET_AGGS_COUNT, return_value=30) + spy_aggs = mocker.patch( + PATCH_GET_AGGS, return_value=list(TEST_AGGREGATORS.values()) + ) + + switch_to_ui_mode(page, "training") + page.open(BASE_URL.format("/aggregators/ui")) + + old_url = page.current_url + page.search("agg1") + page.wait_for_url_change(old_url) + + assert "search=agg1" in page.current_url + spy_aggs.assert_called_with(filters={"search": "agg1", **PAGINATION}) + + old_url = page.current_url + page.set_ordering("Name A–Z") + page.wait_for_url_change(old_url) + + spy_aggs.assert_called_with( + filters={"search": "agg1", "limit": 9, "offset": 0, "ordering": "name"} + ) + + old_url = page.current_url + page.set_page_size(24) + page.wait_for_url_change(old_url) + + spy_aggs.assert_called_with( + filters={"search": "agg1", "limit": 24, "offset": 0, "ordering": "name"} + ) + + old_url = page.current_url + page.click(page.page_link(2)) + page.wait_for_url_change(old_url) + + spy_aggs.assert_called_with( + filters={"search": "agg1", "limit": 24, "offset": 24, "ordering": "name"} + ) From 587f6514ef5db5367f892c1ea8d894161f8e4c04 Mon Sep 17 00:00:00 2001 From: mhmdk0 Date: Mon, 10 Aug 2026 18:59:26 +0300 Subject: [PATCH 37/48] update training experiments tests --- .../tests/pages/training/details_page.py | 48 +- .../web_ui/tests/pages/training/ui_page.py | 24 +- .../test_training_experiment_details_page.py | 701 +++++++++++++++++- .../test_training_experiments_page.py | 113 ++- 4 files changed, 835 insertions(+), 51 deletions(-) diff --git a/cli/medperf/web_ui/tests/pages/training/details_page.py b/cli/medperf/web_ui/tests/pages/training/details_page.py index ca2f5272f..3b50d53d0 100644 --- a/cli/medperf/web_ui/tests/pages/training/details_page.py +++ b/cli/medperf/web_ui/tests/pages/training/details_page.py @@ -79,16 +79,24 @@ class TrainingDetailsPage(BasePage): "//script[contains(., 'resumeRunningTask(\"#close-event-form\")')]", ) - AGGREGATOR_SELECT = ( - By.CSS_SELECTOR, - "#set-aggregator-form select[name='aggregator_id']", - ) + # The aggregator picker is a searchable_select widget (hidden input + + # JS-driven query/listbox), not a real