"""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