Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c6a71fb
remove benchmark workflow and container comp. tests pages
mhmdk0 May 2, 2026
209434c
use get_task_id as a helper function to be mocked during testing
mhmdk0 May 2, 2026
a69a708
add `data-testid` attributes for aggregator pages - unit tests
mhmdk0 May 4, 2026
414477f
add `data-testid` attributes for benchmark pages - unit tests
mhmdk0 May 4, 2026
a77f088
add `data-testid` attributes for container pages - unit tests
mhmdk0 May 4, 2026
42fb2b6
add `data-testid` attributes for dataset pages - unit tests
mhmdk0 May 4, 2026
951acaf
add `data-testid` attributes for macros - update with task_running -…
mhmdk0 May 4, 2026
a0df3db
add `data-testid` attributes for navbar - unit tests
mhmdk0 May 4, 2026
9b32998
add `data-testid` attributes for training experiment pages - unit tests
mhmdk0 May 4, 2026
2d35374
fix enabled button in aggregator registration when a task is running
mhmdk0 May 4, 2026
f34e2fc
add page object models for aggregator
mhmdk0 May 4, 2026
d0480f7
add page object models for training experiments
mhmdk0 May 4, 2026
e7cfcae
update page object models for dataset, benchmark, container, securty,…
mhmdk0 May 4, 2026
372d0e0
fix data-prep containers not showing (non-owner) in training registra…
mhmdk0 May 4, 2026
974a091
update tests config for training e2e tests
mhmdk0 May 4, 2026
ae38e36
add helpers for training e2e tests
mhmdk0 May 4, 2026
ccb0778
add training e2e tests
mhmdk0 May 4, 2026
dd0df60
update login redirection assertion to support training
mhmdk0 May 4, 2026
af13989
add unit tests for evaluation and training
mhmdk0 May 4, 2026
3382899
update `webui-ci.yml`
mhmdk0 May 4, 2026
906fbae
add `webui-training-ci.yml` and `webui-unittests.yml` files
mhmdk0 May 4, 2026
a328b18
Merge branch 'main' into webui-tests
mhmdk0 May 4, 2026
ba25ca4
fix CI errors
mhmdk0 May 4, 2026
4834db8
Merge branch 'main' into webui-tests
mhmdk0 Aug 7, 2026
3a13850
add data-testid attributes
mhmdk0 Aug 10, 2026
8f735cc
add TestModel mock for webui tests
mhmdk0 Aug 10, 2026
8893262
fix get_container_type - webui utils
mhmdk0 Aug 10, 2026
e52ee99
add TestAsset mockup for webui tests
mhmdk0 Aug 10, 2026
71bc859
remove yaml-fetch
mhmdk0 Aug 10, 2026
c1118ce
remove yaml-fetch from app
mhmdk0 Aug 10, 2026
160dddb
update and add new tests to settings page
mhmdk0 Aug 10, 2026
65e4214
add api tests for entity_search, logout, events
mhmdk0 Aug 10, 2026
706c500
update login tests
mhmdk0 Aug 10, 2026
95a8745
update security page tests
mhmdk0 Aug 10, 2026
3b1c003
update navbar tests
mhmdk0 Aug 10, 2026
f816ba5
add helpers for tests
mhmdk0 Aug 10, 2026
ac42f6c
update conftest
mhmdk0 Aug 10, 2026
71190e0
update aggregators tests
mhmdk0 Aug 10, 2026
587f651
update training experiments tests
mhmdk0 Aug 10, 2026
eaa79df
update datasets tests
mhmdk0 Aug 10, 2026
0d35fb9
add assets tests
mhmdk0 Aug 10, 2026
640100d
update benchmarks tests
mhmdk0 Aug 10, 2026
52baa32
update containers tests and add container access tests
mhmdk0 Aug 10, 2026
7c65a4f
add models tests
mhmdk0 Aug 10, 2026
85a896f
fix e2e tests
mhmdk0 Aug 13, 2026
8333942
apply linting
mhmdk0 Aug 13, 2026
09d3a88
fix resolution too deep error
mhmdk0 Aug 13, 2026
4d9aaa0
uncomment headless = true (fix tests)
mhmdk0 Aug 26, 2026
c6b0217
fix training e2e tests
mhmdk0 Aug 27, 2026
d458654
revert prep registration and update mine_only attribute
mhmdk0 Aug 27, 2026
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
8 changes: 4 additions & 4 deletions .github/workflows/webui-ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: WebUI Integration workflow
name: WebUI Evaluation Integration workflow

on: pull_request

jobs:
setup:
name: webui-integration-test
name: webui-evaluation-integration-test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
Expand Down Expand Up @@ -48,6 +48,6 @@ jobs:
working-directory: .
run: medperf_webui & sleep 6

- name: Run webUI tutorial E2E tests with pytest
- name: Run webUI evaluation tutorial E2E tests with pytest
working-directory: ./cli/medperf/web_ui/tests/e2e/
run: pytest
run: pytest test_medperf_tutorial_workflow.py
49 changes: 49 additions & 0 deletions .github/workflows/webui-training-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: WebUI Training Integration workflow

on: pull_request

jobs:
setup:
name: webui-training-integration-test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Python 3.11
uses: actions/setup-python@v2
with:
python-version: '3.11'

- name: Install dependencies
working-directory: .
run: |
python -m pip install --upgrade pip
pip install -r cli/test-requirements.txt
pip install -r server/requirements.txt
pip install -r server/test-requirements.txt
pip install -e cli/

- name: Set server environment vars
working-directory: ./server
run: cp .env.local.local-auth .env

- name: Run postgresql server in background
working-directory: ./server
run: sh run_dev_postgresql.sh && sleep 6

- name: Run django server in background with generated certs
working-directory: ./server
run: sh setup-dev-server.sh & sleep 6

- name: Reset and seed database with tutorial data
working-directory: ./server
run: sh reset_db.sh && python seed.py

- name: Run fastapi server in background
working-directory: .
run: medperf_webui & sleep 6

- name: Run webUI training tutorial E2E tests with pytest
working-directory: ./cli/medperf/web_ui/tests/e2e/
run: pytest test_medperf_training_workflow.py
41 changes: 41 additions & 0 deletions .github/workflows/webui-unittests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: WebUI Unit Tests workflow

on: pull_request

jobs:
setup:
name: webui-unit-tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Python 3.11
uses: actions/setup-python@v2
with:
python-version: '3.11'

- name: Install dependencies
working-directory: .
run: |
python -m pip install --upgrade pip
pip install -r cli/test-requirements.txt
pip install -r server/requirements.txt
pip install -r server/test-requirements.txt
pip install -e cli/

- name: Set server environment vars
working-directory: ./server
run: cp .env.local.local-auth .env

- name: Run postgresql server in background
working-directory: ./server
run: sh run_dev_postgresql.sh && sleep 6

- name: Run django server in background with generated certs
working-directory: ./server
run: sh setup-dev-server.sh & sleep 6

- name: Run webUI unit tests with pytest
working-directory: ./cli/medperf/web_ui/tests/unit/
run: pytest
16 changes: 16 additions & 0 deletions cli/medperf/tests/mocks/asset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from medperf.entities.asset import Asset


class TestAsset(Asset):
__test__ = False

def __init__(self, **kwargs):
defaults = {
"id": 1,
"name": "name",
"asset_hash": "asset_hash",
"asset_url": "local",
"state": "OPERATION",
}
defaults.update(kwargs)
super().__init__(**defaults)
26 changes: 26 additions & 0 deletions cli/medperf/tests/mocks/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from medperf.entities.model import Model


class TestModel(Model):
__test__ = False

def __init__(self, **kwargs):
defaults = {
"id": 1,
"name": "name",
"state": "OPERATION",
"is_valid": True,
"type": "CONTAINER",
"container": {
"id": 1,
"name": "container-name",
"container_config": {"key": "value"},
"parameters_config": {"parameter": "value"},
"additional_files_tarball_url": None,
"state": "OPERATION",
"is_valid": True,
},
"asset": None,
}
defaults.update(kwargs)
super().__init__(**defaults)
2 changes: 0 additions & 2 deletions cli/medperf/web_ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from medperf.web_ui.schemas import WebUITask
from medperf.web_ui.training.routes import router as training_router
from medperf.web_ui.aggregators.routes import router as aggregators_router
from medperf.web_ui.yaml_fetch.routes import router as yaml_fetch_router
from medperf.web_ui.api.routes import router as api_router
from medperf.web_ui.security_check import router as login_router
from medperf.web_ui.events import router as events_router
Expand Down Expand Up @@ -60,7 +59,6 @@ async def dispatch(self, request, call_next):
web_app.include_router(assets_router, prefix="/assets")
web_app.include_router(training_router, prefix="/training")
web_app.include_router(aggregators_router, prefix="/aggregators")
web_app.include_router(yaml_fetch_router)
web_app.include_router(api_router, prefix="/api")
web_app.include_router(login_router)
web_app.include_router(events_router)
Expand Down
3 changes: 1 addition & 2 deletions cli/medperf/web_ui/entity_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,8 @@ def search_entities( # noqa
}

filters = {"search": query, "limit": limit, "offset": 0, "ordering": "name"}
my_user_id = get_medperf_user_data()["id"]
if mine_only:
filters["owner"] = my_user_id
filters["owner"] = get_medperf_user_data()["id"]

items = entity_cls.all(filters=filters)
items = _apply_allowed_ids(items, allowed_ids)
Expand Down
8 changes: 6 additions & 2 deletions cli/medperf/web_ui/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
REDIRECT_BASE = "/aggregators/ui/display/";

function checkAggregatorFormValidity() {
var form = document.getElementById("aggregator-register-form");
var taskRunning = form && form.dataset.taskRunning === "1";
var nameEl = document.getElementById("name");
var addressEl = document.getElementById("address");
var portEl = document.getElementById("port");
Expand All @@ -13,7 +15,7 @@ function checkAggregatorFormValidity() {
var cubeValue = cubeEl && cubeEl.value ? parseInt(cubeEl.value, 10) : 0;
var isValid = nameValue.length > 0 && addressValue.length > 0 && portValue > 0 && portValue <= 65535 && adminPortValue > 0 && adminPortValue <= 65535 && cubeValue > 0;
var btn = document.getElementById("register-aggregator-btn");
if (btn) btn.disabled = !isValid;
if (btn) btn.disabled = taskRunning || !isValid;
}

function init() {
Expand Down
36 changes: 18 additions & 18 deletions cli/medperf/web_ui/templates/aggregators/aggregator_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,59 +11,59 @@
<div class="mb-8">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center mb-3">
<h1 class="text-5xl font-bold text-ink">{{ entity.name }}</h1>
<h1 data-testid="aggregator-header" class="text-5xl font-bold text-ink">{{ entity.name }}</h1>
</div>
</div>
</div>

<div class="bg-card dark:bg-card rounded-2xl p-8 shadow-lg border-2 border-border-brand dark:border-border mb-6">
<div class="flex flex-wrap justify-between items-center gap-4 mb-6">
<h2 class="text-3xl font-bold text-ink flex items-center">
<h2 data-testid="aggregator-details-heading" class="text-3xl font-bold text-ink flex items-center">
<i class="fas fa-info-circle mr-3"></i>Details
</h2>
</div>
<div class="info-highlight rounded-xl p-6 mb-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<p class="text-sm text-muted-fg mb-1 font-semibold">Aggregator ID</p>
<p class="text-xl font-bold text-ink">{{ entity.id }}</p>
<p data-testid="aggregator-id-label" class="text-sm text-muted-fg mb-1 font-semibold">Aggregator ID</p>
<p data-testid="aggregator-id" class="text-xl font-bold text-ink">{{ entity.id }}</p>
</div>
<div>
<p class="text-sm text-muted-fg mb-1 font-semibold">Owner</p>
<p class="text-xl font-bold text-ink">
<p data-testid="aggregator-owner-label" class="text-sm text-muted-fg mb-1 font-semibold">Owner</p>
<p data-testid="aggregator-owner" class="text-xl font-bold text-ink">
<i class="fas fa-user text-brand-accent mr-2"></i>{% if owner %} You {% else %}{{ entity.owner }}{% endif %}
</p>
</div>
</div>
</div>
<div class="mb-6">
<p class="text-sm text-muted-fg mb-2 font-semibold">Address</p>
<p class="text-xl font-mono text-ink">{{ entity.address }}</p>
<p data-testid="aggregator-address-label" class="text-sm text-muted-fg mb-2 font-semibold">Address</p>
<p data-testid="aggregator-address" class="text-xl font-mono text-ink">{{ entity.address }}</p>
</div>
<div class="mb-6">
<p class="text-sm text-muted-fg mb-2 font-semibold">Port</p>
<p class="text-xl font-mono text-ink">{{ entity.port }}</p>
<p data-testid="aggregator-port-label" class="text-sm text-muted-fg mb-2 font-semibold">Port</p>
<p data-testid="aggregator-port" class="text-xl font-mono text-ink">{{ entity.port }}</p>
</div>
<div class="mb-6">
<p class="text-sm text-muted-fg mb-2 font-semibold">Admin Port</p>
<p class="text-xl font-mono text-ink">{{ entity.admin_port }}</p>
<p data-testid="aggregator-admin-port-label" class="text-sm text-muted-fg mb-2 font-semibold">Admin Port</p>
<p data-testid="aggregator-admin-port" class="text-xl font-mono text-ink">{{ entity.admin_port }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 pt-6 border-t-2 border-border">
<div>
<p class="text-sm text-muted-fg mb-2 font-semibold">Created</p>
<span class="text-ink font-medium" data-date="{{ entity.created_at }}"></span>
<p data-testid="aggregator-created-label" class="text-sm text-muted-fg mb-2 font-semibold">Created</p>
<span data-testid="aggregator-created" class="text-ink font-medium" data-date="{{ entity.created_at }}"></span>
</div>
<div>
<p class="text-sm text-muted-fg mb-2 font-semibold">Modified</p>
<span class="text-ink font-medium" data-date="{{ entity.modified_at }}"></span>
<p data-testid="aggregator-modified-label" class="text-sm text-muted-fg mb-2 font-semibold">Modified</p>
<span data-testid="aggregator-modified" class="text-ink font-medium" data-date="{{ entity.modified_at }}"></span>
</div>
</div>
</div>

{% if owner %}
<div class="bg-card dark:bg-card rounded-2xl shadow-lg border-2 border-border-brand dark:border-border mb-6 overflow-hidden">
<div class="px-4 py-3 border-b border-border bg-muted">
<h3 class="mb-0 text-ink font-bold">Training experiments</h3>
<h3 data-testid="aggregator-experiments-heading" class="mb-0 text-ink font-bold">Training experiments</h3>
</div>
<div class="p-6">
{% if experiments_using_aggregator %}
Expand All @@ -83,7 +83,7 @@ <h3 class="mb-0 text-ink font-bold">Training experiments</h3>

<div class="bg-card dark:bg-card rounded-2xl shadow-lg border-2 border-border-brand dark:border-border mb-6 overflow-hidden">
<div class="px-4 py-3 border-b border-border bg-muted">
<h3 class="mb-0 text-ink font-bold">Actions</h3>
<h3 data-testid="aggregator-actions-heading" class="mb-0 text-ink font-bold">Actions</h3>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
Expand Down
24 changes: 12 additions & 12 deletions cli/medperf/web_ui/templates/aggregators/register_aggregator.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,42 @@
{% endif %}

<div class="container mx-auto px-4 py-8 max-w-3xl">
<h1 class="text-3xl font-bold text-ink text-center mb-4">Register New Aggregator</h1>
<h1 data-testid="aggregator-register-header" class="text-3xl font-bold text-ink text-center mb-4">Register New Aggregator</h1>
<p class="text-center text-secondary mb-8">
Register a new aggregator with address, port, and aggregation container.
</p>

<form id="aggregator-register-form" class="space-y-6" action="/aggregators/register" method="post" data-panel-title="Registering aggregator" data-confirm-message="register this aggregator?">
<form id="aggregator-register-form" class="space-y-6" action="/aggregators/register" method="post" data-panel-title="Registering aggregator" data-confirm-message="register this aggregator?" data-task-running="{% if task_running %}1{% else %}0{% endif %}">
<div>
<label for="name" class="block text-secondary font-semibold mb-2">Name</label>
<label data-testid="aggregator-register-name-label" for="name" class="block text-secondary font-semibold mb-2">Name</label>
<div class="flex flex-wrap items-stretch gap-2">
<input name="name" type="text" id="name" class="flex-1 min-w-0 px-4 py-3 rounded-xl border-2 border-border-strong dark:border-border dark:bg-card-muted dark:text-ink focus:outline-none focus:border-brand-accent" placeholder="Aggregator name" {% if task_running %}disabled{% endif %} {% if agg_register_running %}value="{{ form_data.get('name', '') }}"{% endif %} maxlength="128">
<span class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_name }}"><i class="fas fa-info-circle text-lg"></i></span>
<span data-testid="aggregator-register-name-tooltip" class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_name }}"><i class="fas fa-info-circle text-lg"></i></span>
</div>
</div>
<div>
<label for="address" class="block text-secondary font-semibold mb-2">Address</label>
<label data-testid="aggregator-register-address-label" for="address" class="block text-secondary font-semibold mb-2">Address</label>
<div class="flex flex-wrap items-stretch gap-2">
<input name="address" type="text" id="address" class="flex-1 min-w-0 px-4 py-3 rounded-xl border-2 border-border-strong dark:border-border dark:bg-card-muted dark:text-ink focus:outline-none focus:border-brand-accent" placeholder="Hostname or IP" {% if task_running %}disabled{% endif %} {% if agg_register_running %}value="{{ form_data.get('address', '') }}"{% endif %}>
<span class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_address }}"><i class="fas fa-info-circle text-lg"></i></span>
<span data-testid="aggregator-register-address-tooltip" class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_address }}"><i class="fas fa-info-circle text-lg"></i></span>
</div>
</div>
<div>
<label for="port" class="block text-secondary font-semibold mb-2">Port</label>
<label data-testid="aggregator-register-port-label" for="port" class="block text-secondary font-semibold mb-2">Port</label>
<div class="flex flex-wrap items-stretch gap-2">
<input name="port" type="number" id="port" class="flex-1 min-w-0 px-4 py-3 rounded-xl border-2 border-border-strong dark:border-border dark:bg-card-muted dark:text-ink focus:outline-none focus:border-brand-accent" placeholder="Port number" min="1" max="65535" {% if task_running %}disabled{% endif %} {% if agg_register_running %}value="{{ form_data.get('port', '') }}"{% endif %}>
<span class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_port }}"><i class="fas fa-info-circle text-lg"></i></span>
<span data-testid="aggregator-register-port-tooltip" class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_port }}"><i class="fas fa-info-circle text-lg"></i></span>
</div>
</div>
<div>
<label for="admin_port" class="block text-secondary font-semibold mb-2">Admin Port</label>
<label data-testid="aggregator-register-admin-port-label" for="admin_port" class="block text-secondary font-semibold mb-2">Admin Port</label>
<div class="flex flex-wrap items-stretch gap-2">
<input name="admin_port" type="number" id="admin-port" class="flex-1 min-w-0 px-4 py-3 rounded-xl border-2 border-border-strong dark:border-border dark:bg-card-muted dark:text-ink focus:outline-none focus:border-brand-accent" placeholder="Admin Port number" min="1" max="65535" {% if task_running %}disabled{% endif %} {% if agg_register_running %}value="{{ form_data.get('admin_port', '') }}"{% endif %}>
<span class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_admin_port }}"><i class="fas fa-info-circle text-lg"></i></span>
<span data-testid="aggregator-register-admin-port-tooltip" class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_admin_port }}"><i class="fas fa-info-circle text-lg"></i></span>
</div>
</div>
<div>
<label for="aggregation_mlcube" class="block text-secondary font-semibold mb-2">Aggregation Container</label>
<label data-testid="aggregator-register-container-label" for="aggregation_mlcube" class="block text-secondary font-semibold mb-2">Aggregation Container</label>
<div class="flex flex-wrap items-stretch gap-2">
{% set entity_type = "container" %}
{% set input_name = "aggregation_mlcube" %}
Expand All @@ -57,7 +57,7 @@ <h1 class="text-3xl font-bold text-ink text-center mb-4">Register New Aggregator
{% set selected_value = form_data.get('aggregation_mlcube', '') if agg_register_running else '' %}
{% set disabled = task_running %}
{% include "partials/searchable_select.html" %}
<span class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_aggregation_mlcube }}"><i class="fas fa-info-circle text-lg"></i></span>
<span data-testid="aggregator-register-container-tooltip" class="inline-flex items-center px-3 tooltip-icon text-brand-accent" title="{{ tooltips.register_aggregator_aggregation_mlcube }}"><i class="fas fa-info-circle text-lg"></i></span>
</div>
</div>
<div class="text-center pt-4">
Expand Down
Loading
Loading