| | """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, |
| | ) |
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | @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", |
| | ), |
| | ], |
| | ) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | 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 |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | 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 |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | 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" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | 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" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | 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 |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | 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 |
| |
|