TrialPath / app /tests /test_components.py
yakilee's picture
style: apply ruff format to entire codebase
e46883d
"""Tests for app/components/ — Streamlit UI components.
Each component is a pure function that takes data and returns a render-spec dict
(not actual Streamlit widgets) so we can test without a running Streamlit app.
Components:
1. file_uploader - render_file_uploader_spec
2. profile_card - render_profile_card
3. trial_card - render_trial_card
4. gap_card - render_gap_card
5. progress_tracker - render_progress_tracker
6. disclaimer_banner - DISCLAIMER_TEXT, render_disclaimer_spec
"""
import pytest
from trialpath.models import (
Biomarker,
CriterionAssessment,
CriterionDecision,
Demographics,
Diagnosis,
EligibilityLedger,
GapItem,
OverallAssessment,
PatientProfile,
PerformanceStatus,
Treatment,
TrialCandidate,
UnknownField,
)
# ---------------------------------------------------------------------------
# Fixtures — reusable model instances
# ---------------------------------------------------------------------------
@pytest.fixture
def sample_profile():
return PatientProfile(
patient_id="P001",
demographics=Demographics(age=62, sex="Female"),
diagnosis=Diagnosis(
primary_condition="Non-Small Cell Lung Cancer",
histology="adenocarcinoma",
stage="IIIB",
),
performance_status=PerformanceStatus(scale="ECOG", value=1),
biomarkers=[
Biomarker(name="EGFR", result="Exon 19 deletion"),
Biomarker(name="ALK", result="Negative"),
Biomarker(name="PD-L1", result="45%"),
],
treatments=[
Treatment(drug_name="Carboplatin", line=1),
Treatment(drug_name="Pemetrexed", line=1),
],
unknowns=[
UnknownField(field="KRAS", reason="Not in records", importance="high"),
UnknownField(field="Brain MRI", reason="Not available", importance="medium"),
],
)
@pytest.fixture
def sample_trial():
return TrialCandidate(
nct_id="NCT04000001",
title="KEYNOTE-999: Pembrolizumab + Chemo for NSCLC",
conditions=["NSCLC"],
phase="Phase 3",
status="Recruiting",
fingerprint_text="Pembrolizumab combination therapy advanced NSCLC",
)
@pytest.fixture
def sample_ledger():
return EligibilityLedger(
patient_id="P001",
nct_id="NCT04000001",
overall_assessment=OverallAssessment.UNCERTAIN,
criteria=[
CriterionAssessment(
criterion_id="inc_1",
type="inclusion",
text="Confirmed NSCLC diagnosis",
decision=CriterionDecision.MET,
),
CriterionAssessment(
criterion_id="inc_2",
type="inclusion",
text="ECOG 0-1",
decision=CriterionDecision.MET,
),
CriterionAssessment(
criterion_id="inc_3",
type="inclusion",
text="PD-L1 >= 50%",
decision=CriterionDecision.NOT_MET,
),
CriterionAssessment(
criterion_id="exc_1",
type="exclusion",
text="No prior immunotherapy",
decision=CriterionDecision.UNKNOWN,
),
],
gaps=[
GapItem(
description="Brain MRI results needed",
recommended_action="Upload brain MRI report",
clinical_importance="high",
),
],
)
# ===========================================================================
# 1. file_uploader
# ===========================================================================
class TestFileUploader:
def test_spec_has_accepted_types(self):
from app.components.file_uploader import render_file_uploader_spec
spec = render_file_uploader_spec()
assert "pdf" in spec["accepted_types"]
assert "png" in spec["accepted_types"]
assert "jpg" in spec["accepted_types"]
assert "jpeg" in spec["accepted_types"]
def test_spec_allows_multiple_files(self):
from app.components.file_uploader import render_file_uploader_spec
spec = render_file_uploader_spec()
assert spec["accept_multiple_files"] is True
def test_spec_has_label(self):
from app.components.file_uploader import render_file_uploader_spec
spec = render_file_uploader_spec()
assert "label" in spec
assert len(spec["label"]) > 0
def test_format_file_info(self):
from app.components.file_uploader import format_file_info
result = format_file_info("report.pdf", 245_000)
assert "report.pdf" in result
assert "KB" in result
def test_format_file_info_large_file(self):
from app.components.file_uploader import format_file_info
result = format_file_info("scan.pdf", 2_500_000)
assert "MB" in result
# ===========================================================================
# 2. profile_card
# ===========================================================================
class TestProfileCard:
def test_renders_demographics(self, sample_profile):
from app.components.profile_card import render_profile_card
card = render_profile_card(sample_profile)
assert card["demographics"]["age"] == 62
assert card["demographics"]["sex"] == "Female"
def test_renders_diagnosis(self, sample_profile):
from app.components.profile_card import render_profile_card
card = render_profile_card(sample_profile)
assert (
"NSCLC" in card["diagnosis"]["primary_condition"]
or "Non-Small Cell" in card["diagnosis"]["primary_condition"]
)
assert card["diagnosis"]["stage"] == "IIIB"
assert card["diagnosis"]["histology"] == "adenocarcinoma"
def test_renders_biomarkers(self, sample_profile):
from app.components.profile_card import render_profile_card
card = render_profile_card(sample_profile)
assert len(card["biomarkers"]) == 3
names = [b["name"] for b in card["biomarkers"]]
assert "EGFR" in names
assert "PD-L1" in names
def test_renders_treatments(self, sample_profile):
from app.components.profile_card import render_profile_card
card = render_profile_card(sample_profile)
assert len(card["treatments"]) == 2
assert card["treatments"][0]["drug_name"] == "Carboplatin"
def test_renders_unknowns(self, sample_profile):
from app.components.profile_card import render_profile_card
card = render_profile_card(sample_profile)
assert len(card["unknowns"]) == 2
fields = [u["field"] for u in card["unknowns"]]
assert "KRAS" in fields
assert "Brain MRI" in fields
def test_renders_performance_status(self, sample_profile):
from app.components.profile_card import render_profile_card
card = render_profile_card(sample_profile)
assert card["performance_status"]["scale"] == "ECOG"
assert card["performance_status"]["value"] == 1
def test_handles_none_diagnosis(self):
from app.components.profile_card import render_profile_card
profile = PatientProfile(patient_id="P002")
card = render_profile_card(profile)
assert card["diagnosis"] is None
def test_handles_empty_biomarkers(self):
from app.components.profile_card import render_profile_card
profile = PatientProfile(patient_id="P003")
card = render_profile_card(profile)
assert card["biomarkers"] == []
def test_has_minimum_prescreen_flag(self, sample_profile):
from app.components.profile_card import render_profile_card
card = render_profile_card(sample_profile)
assert card["has_minimum_prescreen_data"] is True
def test_has_minimum_prescreen_flag_false(self):
from app.components.profile_card import render_profile_card
profile = PatientProfile(patient_id="P004")
card = render_profile_card(profile)
assert card["has_minimum_prescreen_data"] is False
# ===========================================================================
# 3. trial_card
# ===========================================================================
class TestTrialCard:
def test_renders_basic_info(self, sample_trial, sample_ledger):
from app.components.trial_card import render_trial_card
card = render_trial_card(sample_trial, sample_ledger)
assert card["nct_id"] == "NCT04000001"
assert "KEYNOTE" in card["title"]
assert card["phase"] == "Phase 3"
assert card["status"] == "Recruiting"
def test_renders_traffic_light(self, sample_trial, sample_ledger):
from app.components.trial_card import render_trial_card
card = render_trial_card(sample_trial, sample_ledger)
assert card["traffic_light"] == "yellow"
def test_renders_criteria_counts(self, sample_trial, sample_ledger):
from app.components.trial_card import render_trial_card
card = render_trial_card(sample_trial, sample_ledger)
assert card["met_count"] == 2
assert card["not_met_count"] == 1
assert card["unknown_count"] == 1
def test_renders_criteria_list(self, sample_trial, sample_ledger):
from app.components.trial_card import render_trial_card
card = render_trial_card(sample_trial, sample_ledger)
assert len(card["criteria"]) == 4
first = card["criteria"][0]
assert "criterion_id" in first
assert "text" in first
assert "decision" in first
def test_renders_overall_assessment(self, sample_trial, sample_ledger):
from app.components.trial_card import render_trial_card
card = render_trial_card(sample_trial, sample_ledger)
assert card["overall_assessment"] == "uncertain"
def test_green_traffic_light(self, sample_trial):
from app.components.trial_card import render_trial_card
ledger = EligibilityLedger(
patient_id="P001",
nct_id="NCT04000001",
overall_assessment=OverallAssessment.LIKELY_ELIGIBLE,
criteria=[
CriterionAssessment(
criterion_id="inc_1",
type="inclusion",
text="Has NSCLC",
decision=CriterionDecision.MET,
),
],
)
card = render_trial_card(sample_trial, ledger)
assert card["traffic_light"] == "green"
def test_red_traffic_light(self, sample_trial):
from app.components.trial_card import render_trial_card
ledger = EligibilityLedger(
patient_id="P001",
nct_id="NCT04000001",
overall_assessment=OverallAssessment.LIKELY_INELIGIBLE,
criteria=[
CriterionAssessment(
criterion_id="inc_1",
type="inclusion",
text="Has NSCLC",
decision=CriterionDecision.NOT_MET,
),
],
)
card = render_trial_card(sample_trial, ledger)
assert card["traffic_light"] == "red"
def test_gaps_included(self, sample_trial, sample_ledger):
from app.components.trial_card import render_trial_card
card = render_trial_card(sample_trial, sample_ledger)
assert len(card["gaps"]) == 1
assert card["gaps"][0]["description"] == "Brain MRI results needed"
# ===========================================================================
# 4. gap_card
# ===========================================================================
class TestGapCard:
def test_renders_single_gap(self):
from app.components.gap_card import render_gap_card
gap = GapItem(
description="Brain MRI results needed",
recommended_action="Upload brain MRI report",
clinical_importance="high",
)
card = render_gap_card(gap, affected_trials=["NCT04000001", "NCT04000003"])
assert card["description"] == "Brain MRI results needed"
assert card["recommended_action"] == "Upload brain MRI report"
assert card["clinical_importance"] == "high"
assert len(card["affected_trials"]) == 2
def test_renders_gap_without_affected_trials(self):
from app.components.gap_card import render_gap_card
gap = GapItem(
description="KRAS mutation status",
recommended_action="Request test from oncologist",
clinical_importance="medium",
)
card = render_gap_card(gap)
assert card["affected_trials"] == []
def test_importance_badge(self):
from app.components.gap_card import render_gap_card
gap = GapItem(
description="Test gap",
recommended_action="Do something",
clinical_importance="high",
)
card = render_gap_card(gap)
assert card["importance_color"] == "red"
def test_importance_badge_medium(self):
from app.components.gap_card import render_gap_card
gap = GapItem(
description="Test gap",
recommended_action="Do something",
clinical_importance="medium",
)
card = render_gap_card(gap)
assert card["importance_color"] == "orange"
def test_importance_badge_low(self):
from app.components.gap_card import render_gap_card
gap = GapItem(
description="Test gap",
recommended_action="Do something",
clinical_importance="low",
)
card = render_gap_card(gap)
assert card["importance_color"] == "grey"
# ===========================================================================
# 5. progress_tracker
# ===========================================================================
class TestProgressTracker:
def test_renders_all_states(self):
from app.components.progress_tracker import render_progress_tracker
spec = render_progress_tracker("INGEST")
assert len(spec["steps"]) == 5
def test_ingest_is_current(self):
from app.components.progress_tracker import render_progress_tracker
spec = render_progress_tracker("INGEST")
steps = spec["steps"]
assert steps[0]["status"] == "current"
assert steps[1]["status"] == "upcoming"
def test_middle_state(self):
from app.components.progress_tracker import render_progress_tracker
spec = render_progress_tracker("VALIDATE_TRIALS")
steps = spec["steps"]
assert steps[0]["status"] == "completed"
assert steps[1]["status"] == "completed"
assert steps[2]["status"] == "current"
assert steps[3]["status"] == "upcoming"
assert steps[4]["status"] == "upcoming"
def test_summary_state(self):
from app.components.progress_tracker import render_progress_tracker
spec = render_progress_tracker("SUMMARY")
steps = spec["steps"]
for i in range(4):
assert steps[i]["status"] == "completed"
assert steps[4]["status"] == "current"
def test_step_labels(self):
from app.components.progress_tracker import render_progress_tracker
spec = render_progress_tracker("INGEST")
labels = [s["label"] for s in spec["steps"]]
assert "Upload" in labels[0]
assert "Summary" in labels[4] or "Export" in labels[4]
def test_current_index(self):
from app.components.progress_tracker import render_progress_tracker
spec = render_progress_tracker("GAP_FOLLOWUP")
assert spec["current_index"] == 3
# ===========================================================================
# 6. disclaimer_banner
# ===========================================================================
class TestDisclaimerBanner:
def test_disclaimer_text_content(self):
from app.components.disclaimer_banner import DISCLAIMER_TEXT
text_lower = DISCLAIMER_TEXT.lower()
assert "information" in text_lower or "educational" in text_lower
assert "medical advice" in text_lower
def test_disclaimer_spec(self):
from app.components.disclaimer_banner import render_disclaimer_spec
spec = render_disclaimer_spec()
assert spec["type"] == "info"
assert len(spec["text"]) > 0
def test_disclaimer_not_empty(self):
from app.components.disclaimer_banner import DISCLAIMER_TEXT
assert len(DISCLAIMER_TEXT) > 20