Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one commit with no earlier history: the tracked files were de-identified first (synthetic claim ids and patient data only; see Context.md section 7). Credentials, run logs, screenshots, claim reports and client workbooks are gitignored and not included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
199 lines
7.0 KiB
Python
199 lines
7.0 KiB
Python
r"""
|
|
Progress Note extraction, against a real browser.
|
|
|
|
python tests\test_progress_note.py
|
|
|
|
Local headless chromium against `tests\fixtures\progress_note_host.html`. Never
|
|
touches eCW: no proxy, no login, no 2FA mail, no attempt against the guard.
|
|
|
|
The fixture puts the note in an **iframe** with Close on the parent page,
|
|
because that is how it actually arrives — the note is a whole printable
|
|
document with its own `<html>`. That split is the trap being tested:
|
|
|
|
* reading the modal's `innerText` finds nothing, since iframe content is not
|
|
part of it — which an earlier version of `open_progress_notes` did;
|
|
* looking for Close inside the note finds nothing either, since it lives on
|
|
the parent.
|
|
|
|
The rest is about eCW's own labels. Every section carries
|
|
`prisma-section="..."`, so the note comes back as a dict keyed by those rather
|
|
than as prose — but `"Footer"` repeats four times, and a naive dict build drops
|
|
the signature block. That is covered.
|
|
"""
|
|
|
|
import sys
|
|
import pathlib
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
|
|
HOST = pathlib.Path(__file__).resolve().parent / "fixtures" / "progress_note_host.html"
|
|
|
|
from ecw_bot.pages import progress_note # noqa: E402
|
|
from ecw_bot.selectors import claim_form as sel # noqa: E402
|
|
|
|
CHECKS = []
|
|
|
|
|
|
def check(fn):
|
|
CHECKS.append(fn)
|
|
return fn
|
|
|
|
|
|
@check
|
|
def found_in_the_iframe_not_the_modal(page, note, where):
|
|
"""The note is a separate document; the frame walk is what finds it."""
|
|
assert where == "frame", where
|
|
# And the trap: the hosting modal's own innerText holds none of the note.
|
|
modal_text = page.evaluate(
|
|
"""() => {
|
|
const m = document.querySelector('.modal');
|
|
return m ? (m.innerText || '') : '';
|
|
}""")
|
|
assert "R69" not in modal_text, (
|
|
"the modal's innerText contains the note, so this fixture no longer "
|
|
"reproduces the iframe case it exists for")
|
|
|
|
|
|
@check
|
|
def patient_header_and_dos(page, note, where):
|
|
hdr = note["patient_header"]
|
|
assert "SAMPLE, Pat Q" in hdr, hdr
|
|
assert "900123" in hdr, hdr
|
|
assert note["dos"] == "06/29/2026", note["dos"]
|
|
|
|
|
|
@check
|
|
def sections_are_keyed_by_ecw_labels(page, note, where):
|
|
got = note["sections"]
|
|
for want in ("Header", "Past Medical History", "Surgical History",
|
|
"Family History", "Social History",
|
|
"Hospitalization/Major Diagnostic Procedure",
|
|
"Reason for Appointment", "History of Present Illness",
|
|
"Assessments", "Labs", "Procedure Codes",
|
|
"Care Plan Details", "Footer", "Active Problem List"):
|
|
assert want in got, f"{want!r} missing; got {sorted(got)}"
|
|
assert "Generalized Anxiety Disorder" in got["Past Medical History"], \
|
|
got["Past Medical History"]
|
|
assert "BH Outreach" in got["Reason for Appointment"], \
|
|
got["Reason for Appointment"]
|
|
# Present but empty on this note, exactly as eCW served it.
|
|
assert got["Active Problem List"] == "", repr(got["Active Problem List"])
|
|
|
|
|
|
@check
|
|
def repeated_footer_sections_are_joined_not_overwritten(page, note, where):
|
|
"""
|
|
"Footer" appears four times. Overwriting on each would keep only the last —
|
|
the facility block — and silently lose the signature and the sign-off.
|
|
"""
|
|
footer = note["sections"]["Footer"]
|
|
assert "Electronically signed by Provider" in footer, footer
|
|
assert "Sign off status" in footer, footer
|
|
assert "Brighton Womens Clinic" in footer, footer
|
|
|
|
|
|
@check
|
|
def assessments_are_parsed_with_their_codes(page, note, where):
|
|
a = note["assessments"]
|
|
assert len(a) == 2, a
|
|
assert a[0]["n"] == "1" and a[0]["code"] == "R69", a[0]
|
|
assert a[0]["primary"] is True, a[0]
|
|
assert "Illness, unspecified" in a[0]["text"], a[0]
|
|
assert a[1]["code"] == "F41.1", a[1]
|
|
assert a[1]["primary"] is False, a[1]
|
|
|
|
|
|
@check
|
|
def procedure_codes_carry_their_modifiers(page, note, where):
|
|
"""
|
|
The pair charge entry actually needs: the note's CPT and modifier are the
|
|
same H0023 / GT the UB Claim form shows.
|
|
"""
|
|
pr = note["procedure_codes"]
|
|
assert len(pr) == 2, pr
|
|
assert pr[0]["code"] == "H0023", pr[0]
|
|
assert pr[0]["modifiers"] == ["GT"], pr[0]
|
|
assert "Behavioral Health Outreach Service" in pr[0]["description"], pr[0]
|
|
# The description must not swallow the "Modifiers: ..." tail.
|
|
assert "Modifiers" not in pr[0]["description"], pr[0]
|
|
assert pr[1]["code"] == "90791" and pr[1]["modifiers"] == [], pr[1]
|
|
|
|
|
|
@check
|
|
def signoff_is_extracted(page, note, where):
|
|
assert "Morgan Lee Taylor" in note["signed_by"], note["signed_by"]
|
|
assert "07/13/2026" in note["signed_at"], note["signed_at"]
|
|
assert note["signoff_status"] == "Completed", note["signoff_status"]
|
|
|
|
|
|
@check
|
|
def chart_locked_needs_textcontent(page, note, where):
|
|
"""
|
|
`#chartLockedId` is `display:none`. innerText returns "" for it, so a
|
|
reader using innerText alone reports the chart as unlocked — the opposite
|
|
of the truth, on a field that decides whether a claim should be touched.
|
|
"""
|
|
assert note["chart_locked"] is True, note["chart_locked"]
|
|
|
|
|
|
@check
|
|
def summary_names_the_facts_that_matter(page, note, where):
|
|
text = progress_note.summary(note)
|
|
for want in ("06/29/2026", "R69", "H0023", "GT", "Completed",
|
|
"Morgan Lee Taylor"):
|
|
assert want in text, f"{want!r} missing from summary:\n{text}"
|
|
|
|
|
|
@check
|
|
def close_clicks_the_parent_not_the_note(page, note, where):
|
|
"""Close lives on the host page; the ng-click typo is eCW's own."""
|
|
assert "closeProgressNoteViwer" in sel.SEL_PN_CLOSE, sel.SEL_PN_CLOSE
|
|
assert progress_note.close(page) is True
|
|
assert page.locator(".modal").first.is_visible() is False
|
|
|
|
|
|
def main() -> int:
|
|
if not HOST.exists():
|
|
print(f" FAIL fixture missing: {HOST}")
|
|
return 1
|
|
try:
|
|
from playwright.sync_api import sync_playwright
|
|
except ImportError:
|
|
print(" SKIP playwright is not installed")
|
|
return 0
|
|
|
|
failures = 0
|
|
with sync_playwright() as pw:
|
|
browser = pw.chromium.launch(headless=True) # local only, no proxy
|
|
page = browser.new_page()
|
|
page.goto(HOST.as_uri())
|
|
page.wait_for_timeout(500) # let the iframe load
|
|
|
|
reader, where = progress_note.find(page)
|
|
if reader is None:
|
|
print(" FAIL the note document was not found at all")
|
|
browser.close()
|
|
return 1
|
|
note = progress_note.extract(reader)
|
|
|
|
for fn in CHECKS:
|
|
name = fn.__name__.replace("_", " ")
|
|
try:
|
|
fn(page, note, where)
|
|
print(f" ok {name}")
|
|
except AssertionError as exc:
|
|
print(f" FAIL {name}: {exc}")
|
|
failures += 1
|
|
except Exception as exc:
|
|
print(f" ERROR {name}: {type(exc).__name__}: {exc}")
|
|
failures += 1
|
|
browser.close()
|
|
|
|
print(f"\n{len(CHECKS) - failures}/{len(CHECKS)} passed")
|
|
return 1 if failures else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|