Initial import: eCW Charge Validation bot and results Portal
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>
This commit is contained in:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Every CSS selector the bot uses, grouped by screen.
|
||||
|
||||
Deliberately separate from `settings.py`. Both used to live in one 442-line
|
||||
config module, so changing a date range and changing a DOM selector meant
|
||||
editing the same file — and they change for entirely different reasons: these
|
||||
move when eCW ships a new build, settings move when the process changes.
|
||||
|
||||
No module in here imports anything. They are data.
|
||||
"""
|
||||
@@ -0,0 +1,42 @@
|
||||
r"""
|
||||
Account Inquiry ("Patient Inquiry Detail") — opened from the claim form's
|
||||
side menu, read for a patient's earlier claims and their CPT lines
|
||||
(condition #27-#29, `conditions/medicare_wellness_history.py`).
|
||||
|
||||
From Dom.txt (operator's capture, 2026-10-20); the launcher from the
|
||||
claim form DOM the operator pasted earlier (side panel, `vm.panelItems`).
|
||||
"""
|
||||
|
||||
# The claim form's side panel item:
|
||||
# <a class="cursor ng-scope" title="Account Inquiry" ng-click=
|
||||
# "vm.loadSmartPanel(item);"> ... <label class="ft-label">Account
|
||||
# Inquiry</label></a>
|
||||
# Every panel item shares the ng-click; the title tells them apart.
|
||||
SEL_LAUNCHER = 'a[title="Account Inquiry"][ng-click^="vm.loadSmartPanel("]'
|
||||
|
||||
# The modal's close (X) button — `(false)`: close without saving anything.
|
||||
# `(true)` is the footer OK, which is not used.
|
||||
SEL_CLOSE = ('button.close[ng-click='
|
||||
'"validateandclosePatientInquiryDetailModal(false)"]')
|
||||
|
||||
# The modal's own loading overlay.
|
||||
SEL_LOADER = "#accountsLoadingImg"
|
||||
|
||||
# The filter checkboxes are one ng-repeat; "Show CPT Payments" is told
|
||||
# apart by its label text.
|
||||
SEL_SHOW_CPT_PAYMENTS = ('label:has-text("Show CPT Payments") '
|
||||
'input[type="checkbox"]')
|
||||
|
||||
# One per claim; holds the claim's main row, its payment rows and their
|
||||
# CPT rows in nested tables.
|
||||
SEL_CLAIM_ROW = 'tr[ng-repeat^="claim in claims"]'
|
||||
SEL_CLAIM_NO = '[ng-bind="claim.ClaimId"]'
|
||||
SEL_CLAIM_DATE = '[ng-bind^="claim.ServiceDate"]'
|
||||
# "G0438:ANNUAL WELLNESS…" — the code before the colon. Under each posted
|
||||
# payment / adjustment (`payment.Charges`), and in the CPT totals rows.
|
||||
SEL_CPT_DESCRIPTION = ('tr.cptPaymentRow td, tr.cptPaymentSubTotalRow td')
|
||||
|
||||
# Paging.
|
||||
SEL_PAGE_NO = '[ng-model="curpage"]'
|
||||
SEL_PAGE_COUNT = '[ng-bind="totalPage"]'
|
||||
SEL_NEXT = '[ng-click="pibuttonnext()"]'
|
||||
@@ -0,0 +1,394 @@
|
||||
"""
|
||||
The UB Claim form — the modal that opens when a claim number is clicked.
|
||||
|
||||
# The one rule for this screen: NEVER use an id with a number in it.
|
||||
|
||||
eCW builds this modal with a per-open timestamp baked into most ids:
|
||||
|
||||
<div id="UBClaimDiv1788790287089" ...> the backdrop
|
||||
<div id="claimedi1788790287089"> a tab pane
|
||||
<select id="claimStatusSel1788790287089"> claim status
|
||||
<table id="claimCPTCodesTable1788790287089"> the CPT grid
|
||||
|
||||
`1788790287089` is `Date.now()` at the moment the modal opened. It is
|
||||
**different for every claim, and different for the same claim opened twice**.
|
||||
Any selector containing it works exactly once and then silently matches
|
||||
nothing — the worst failure shape, because the run looks fine and the data
|
||||
comes back empty.
|
||||
|
||||
So everything below is matched on one of three stable things:
|
||||
|
||||
* an id with no number in it — `#UBClaimProgressNoteBtn`, `#BillType`
|
||||
* an `ng-model` / `ng-bind` — `[ng-model="ClaimData.ClaimDate"]`
|
||||
* an `ng-click` — `a[ng-click*="cptTabUpdate('cptHcpcs')"]`
|
||||
|
||||
Angular attributes are the best handles here: they are what eCW's own code
|
||||
binds to, so they change only when the underlying data model changes, which is
|
||||
far rarer than a markup reshuffle.
|
||||
|
||||
Note the pairs that look interchangeable and are not:
|
||||
|
||||
* `#UBClaimDiv1788790287089` (backdrop, timestamped) vs **`#UBClaimDiv`**
|
||||
(the modal-dialog inside it, no timestamp). Use the latter.
|
||||
* `#UBClaimBtn18` is **OK — it saves** (`saveAllData(true)`).
|
||||
`#UBClaimBtn17` is Cancel (`closeUBClaimScreen()`). Reading a claim must
|
||||
close with Cancel; clicking OK writes to eCW.
|
||||
"""
|
||||
|
||||
# ── The modal itself ──────────────────────────────────────────────────────────
|
||||
# The inner modal-dialog. Its id carries no timestamp, unlike the backdrop's.
|
||||
SEL_MODAL = "#UBClaimDiv"
|
||||
SEL_MODAL_TITLE = "#UBClaimDiv h4.modal-title"
|
||||
|
||||
# Claim No is text in a cell, not an input — `<span title="9104000">9104000</span>`.
|
||||
SEL_CLAIM_NO_TEXT = "#UBClaimDiv .ubClaimNumberText span"
|
||||
|
||||
# Cancel closes without saving. **Not** #UBClaimBtn18, which is OK and saves.
|
||||
# Fallback (core/fallback.py): Cancel's ng-click, closeUBClaimScreen() —
|
||||
# never the X (`.close`, #UBClaimBtn3), never OK (saveAllData).
|
||||
CANCEL_CHAIN = ("#UBClaimBtn17",
|
||||
'#UBClaimDiv button[ng-click*="closeUBClaimScreen("]'
|
||||
':not(.close):not(#UBClaimBtn3)')
|
||||
SEL_CANCEL = ", ".join(CANCEL_CHAIN)
|
||||
SEL_OK = "#UBClaimBtn18"
|
||||
SEL_CLOSE_X = "#UBClaimBtn3"
|
||||
|
||||
# A stable marker for the UB Claim form specifically (institutional claims —
|
||||
# Medicaid, in practice). eCW opens a DIFFERENT screen with a different
|
||||
# button set entirely for professional/HCFA claims (Commercial payers), so
|
||||
# this is checked before anything below assumes it is looking at a UB form.
|
||||
SEL_IS_UB_FORM = "#UBClaimBtn17, #BillType"
|
||||
|
||||
# The professional/HCFA "Claim" screen's own Cancel — verified against a live
|
||||
# claim (billingClaimBtn36, ng-click="closeClaimScreen()"). Matched on
|
||||
# ng-click rather than the id: the numbered `billingClaimBtnNN` ids look
|
||||
# sequential rather than per-open, but only one capture has been seen, and a
|
||||
# hidden/shown button elsewhere on that toolbar (isERClaim, etc.) could shift
|
||||
# the numbering — ng-click is what actually fires the close, so it is the
|
||||
# more trustworthy handle. Used only as a fallback in close_claim(): this
|
||||
# module's own write path (save_claim, set_cpt_pos_modifier) is UB-form-only
|
||||
# for now, so nothing here targets this screen's OK/Prog.Notes buttons yet.
|
||||
SEL_CANCEL_HCFA = 'button[ng-click="closeClaimScreen()"]'
|
||||
|
||||
# The Dental Claim screen's own Cancel — a THIRD claim-form shape entirely,
|
||||
# distinct from both the UB form and the professional/HCFA "Claim" screen
|
||||
# (verified against a live claim, 9125000): its own modal
|
||||
# (`id="dentalClaim<timestamp>"`), `closeDntl()` instead of
|
||||
# `closeUBClaimScreen()`/`closeClaimScreen()`, `saveData()` instead of
|
||||
# `saveAllData(true)`/`saveAllData()`, and a completely different tab set
|
||||
# ("ICD & CDT" / "Services Rendered" rather than "ICD & CPT" / "CPT/HCPCS").
|
||||
# Its own Cancel carries a stable id with no per-open timestamp, so it is
|
||||
# matched directly rather than by ng-click the way SEL_CANCEL_HCFA is.
|
||||
# Fallback (core/fallback.py): its closeDntl() ng-click, never the X.
|
||||
CANCEL_DENTAL_CHAIN = ("#cancelDentalClaimButton",
|
||||
'button[ng-click*="closeDntl("]:not(.close)')
|
||||
SEL_CANCEL_DENTAL = ", ".join(CANCEL_DENTAL_CHAIN)
|
||||
|
||||
# A stable marker for the Dental Claim screen specifically — reused as both
|
||||
# "is this a Dental claim" and "how do I close it", the same way SEL_CANCEL
|
||||
# doubles as half of SEL_IS_UB_FORM.
|
||||
SEL_IS_DENTAL_FORM = SEL_CANCEL_DENTAL
|
||||
|
||||
# eCW's own "Account Inquiry" smart panel — NOT anything this codebase opens
|
||||
# on purpose. Seen live (claim 9128000, Logs\run_20260915_195515.log):
|
||||
# `id="claimModal<timestamp>" class="modal fade bluetheme billing-width
|
||||
# right-panel-mode in"`, whose backdrop div (`window-class="claimModalBackdrop"`,
|
||||
# also carrying `ng-click="close($event)"` — it is designed to be dismissible)
|
||||
# sat over the results grid and blocked the click meant for the claim-number
|
||||
# link underneath, for the full 30s click timeout, before open_claim() ever
|
||||
# got a chance to open the form. Matched on the class Playwright's own retry
|
||||
# trace named, not the id — timestamped, same as everything else on this
|
||||
# screen (see the module docstring).
|
||||
SEL_STRAY_MODAL_BACKDROP = ".claimModalBackdrop"
|
||||
|
||||
# The HCFA "Claim" form's OWN angular-ui-bootstrap window carries that same
|
||||
# class (`window-class="claimModalBackdrop"`, verified live on claims 9109000
|
||||
# and 9119000, 2026-09-29) — the 9128000 element above, `id="claimModal<ts>"
|
||||
# ... right-panel-mode`, is this form's inner modal. Escape dismisses the
|
||||
# window WITHOUT running closeClaimScreen(), so only a window with no HCFA
|
||||
# Cancel inside it is stray — before a claim is opened too: a form still up
|
||||
# then is Cancelled by claim_form._close_leftover_form() (review,
|
||||
# 2026-10-14; eCW opens the next HCFA claim's form by itself after the
|
||||
# Lookup, live run 2026-10-13).
|
||||
SEL_STRAY_MODAL_OVER_CLAIM = (
|
||||
f"{SEL_STRAY_MODAL_BACKDROP}:not(:has({SEL_CANCEL_HCFA}))")
|
||||
|
||||
# What open_claim() actually waits for: ANY of the three screens' own
|
||||
# live-verified markers, since a claim id can open any one of them and
|
||||
# there is no single container id common to all three (the UB form's
|
||||
# backdrop carries a per-open timestamp same as everything else on that
|
||||
# screen — see the module docstring — and no equivalent has been captured
|
||||
# for the HCFA or Dental screens). Waiting on #UBClaimDiv alone cost 5 real
|
||||
# minutes (NAV_TIMEOUT) per HCFA claim before this: the form was genuinely
|
||||
# open on screen the whole time, just never matching what the wait was
|
||||
# looking for. The same bug shape applies to Dental claims (9125000) —
|
||||
# SEL_CANCEL_DENTAL is included for the same reason SEL_CANCEL_HCFA is.
|
||||
SEL_ANY_CLAIM_FORM_OPEN = f"{SEL_IS_UB_FORM}, {SEL_CANCEL_HCFA}, {SEL_CANCEL_DENTAL}"
|
||||
|
||||
|
||||
# ── "Has Angular finished filling it?" (claim_form.wait_claim_loaded) ────────
|
||||
# open_claim() returns the moment a screen's Cancel is visible, and on the
|
||||
# HCFA and Dental screens that is BEFORE Angular has filled the form: a live
|
||||
# claim read 0 CPT rows, Prog. Notes "disabled" and blank Billing Notes, and
|
||||
# went to the summary as Pass (run_20260930_163006; review item D7). The UB
|
||||
# form already waits on SEL_CLAIM_NO_TEXT; these give the other two screens
|
||||
# the same identity check plus "the service lines have codes in them".
|
||||
#
|
||||
# HCFA claim number — `<span ng-bind="ClaimData.InvId">9103000</span>`,
|
||||
# mapped from the live HCFA screen (claim 9103000; tests/fixtures/
|
||||
# hcfa_claim.html reproduces it). Text, not an input.
|
||||
SEL_HCFA_CLAIM_NO = '[ng-bind="ClaimData.InvId"]'
|
||||
# HCFA CPT code inputs: the CPTROW_ rows' `data-fieldname="claimCPTCode"`
|
||||
# cell — the same HCFA fallback pair extract()'s HCFA_FIELDNAME map reads.
|
||||
SEL_HCFA_CPT_CODE = 'tr[id^="CPTROW_"] [data-fieldname="claimCPTCode"]'
|
||||
# Dental claim number and service-line code inputs — mapped from the live
|
||||
# Dental DOM (claim 9125000's screen), scoped under the Dental modal
|
||||
# (`id="dentalClaim<timestamp>"`, matched by prefix for that reason). The
|
||||
# code values are Angular properties: read `.value`, never the attribute.
|
||||
SEL_DENTAL_CLAIM_NO = '[id^="dentalClaim"] [ng-bind="ClaimData.claimNo"]'
|
||||
SEL_DENTAL_CODE = '[id^="dentalClaim"] [data-fieldname="web_cptcode"]'
|
||||
# The window each screen's form lives in, found by walking up (`closest()`)
|
||||
# from that screen's VISIBLE Cancel — so the claim number and codes are read
|
||||
# from the form actually on screen, never from a hidden copy of an earlier
|
||||
# claim's form left in the DOM (eCW does leave hidden forms behind: see
|
||||
# claim_form.any_claim_form_open). The HCFA form's own angular-ui-bootstrap
|
||||
# window is `.claimModalBackdrop` (SEL_STRAY_MODAL_BACKDROP above, verified
|
||||
# live on claims 9109000 and 9119000); the Dental form's is the `.modal`
|
||||
# element whose id is `dentalClaim<timestamp>` (claim 9125000; tests/
|
||||
# fixtures/dental_claim.html reproduces it). The `.modal` class is part of
|
||||
# the selector on purpose: `closest()` stops at the NEAREST match, and the
|
||||
# id prefix alone already matches the inner `dentalClaimModalContent<ts>`
|
||||
# (harmless — it holds the whole form) and would match any other
|
||||
# `dentalClaim…`-id wrapper eCW puts around the footer or Cancel. Stopping
|
||||
# there, the window would hold neither the claim number nor the service
|
||||
# lines, and every Dental claim would wait out the full load timeout and come
|
||||
# back not evaluated (review of unit C, 2026-10-05). Only the modal itself
|
||||
# carries both the id prefix and the `modal` class.
|
||||
SEL_HCFA_WINDOW = SEL_STRAY_MODAL_BACKDROP
|
||||
SEL_DENTAL_WINDOW = '.modal[id^="dentalClaim"]'
|
||||
|
||||
|
||||
# ── Tabs across the middle ────────────────────────────────────────────────────
|
||||
# Matched on ng-click, because every data-target carries the timestamp.
|
||||
SEL_TAB_CPT_REV = "a[ng-click*=\"cptTabUpdate('cptRev')\"]"
|
||||
SEL_TAB_CPT_HCPCS = "a[ng-click*=\"cptTabUpdate('cptHcpcs')\"]"
|
||||
|
||||
# The CPT/HCPCS grid's own wrapper — a rare stable id on this screen.
|
||||
SEL_CPT_GRID = "#CPTCodesDiv"
|
||||
|
||||
# The four upper tabs (ICD/CPT/Revenue, Occurrence, Insurance, Additional).
|
||||
# `data-target` is timestamped, so these go by their stable link ids.
|
||||
SEL_TAB_ICD_CPT_REV = "#UBClaimLink4"
|
||||
SEL_TAB_OCCURRENCE = "#UBClaimLink3"
|
||||
SEL_TAB_INSURANCE = "#UBClaimLink2"
|
||||
SEL_TAB_ADDITIONAL = "#UBClaimLink1"
|
||||
|
||||
|
||||
# ── Claim Status (document #13) ───────────────────────────────────────────────
|
||||
# The claim form's Claim Status dropdown, from the claim form DOMs the
|
||||
# operator pasted: HCFA `select#claimStatusSel<ts>[ng-model="ClaimData.
|
||||
# ClaimStatus"]`, Dental `select#DentalClaimStatus<ts>` (same ng-model), UB
|
||||
# `select#claimStatusSel<ts>[ng-model="selectedClaimStatusCodes"]`. Option
|
||||
# values are the status codes ("PAT" = Patient) on all three. NOT
|
||||
# `select#status[ng-model="claimRP.claimRPStatus"]`, a different field
|
||||
# every form also has.
|
||||
SEL_CLAIM_STATUS = ('select[ng-model="ClaimData.ClaimStatus"], '
|
||||
'select[ng-model="selectedClaimStatusCodes"]')
|
||||
|
||||
|
||||
# ── CPT/HCPCS row-level write targets ─────────────────────────────────────────
|
||||
# Matched by id PREFIX, not the full id: every CPT row's per-field ids end in
|
||||
# the row index (`uBClaimPOS0`, `uBClaimPOS1`, ...) with no timestamp — the
|
||||
# same prefix-matching claim_form.extract()'s own JS already relies on for
|
||||
# reading. `SEL_CPT_ROWS` finds the real rows only: eCW's blank padding rows
|
||||
# and the trailing "*" entry row carry no `CPTROW_` id at all. `CPTROW_` itself
|
||||
# is shared by both the UB form and the professional/HCFA screen (verified
|
||||
# live, claim 9103000) — only the row's OWN field ids differ between them.
|
||||
#
|
||||
# The Dental Claim screen (2026-10-12: Dental claims run the conditions too)
|
||||
# has its own grid shape, from Dom.txt's capture: rows are
|
||||
# `tr[ng-repeat="CPT in CPTCodes track by $index"]` with no CPTROW_ id, and
|
||||
# every field goes by `data-fieldname` (web_cptcode, web_icd1-4, web_units,
|
||||
# web_billedFee) — the third alternative in each selector below. No POS and
|
||||
# no modifier columns at all. Scoped to the Dental window ON SCREEN
|
||||
# (Playwright's `:visible`): eCW leaves closed forms in the DOM, and a
|
||||
# hidden Dental grid must never add rows to a UB/HCFA claim's walk.
|
||||
_DENTAL_ROW = ('.modal[id^="dentalClaim"]:visible '
|
||||
'tr[ng-repeat^="CPT in CPTCodes"]')
|
||||
SEL_CPT_ROWS = f'tr[id^="CPTROW_"], {_DENTAL_ROW}'
|
||||
|
||||
# The HCFA screen's per-field ids (`billingClaimIpt4ngR0`, ...) are a single
|
||||
# sequential numbering shared across the ENTIRE modal, not just this grid —
|
||||
# the same numbering-can-shift risk already noted on SEL_CANCEL_HCFA — so
|
||||
# they are not safe to match by prefix the way the UB form's are.
|
||||
# `data-fieldname` is what is actually stable there, verified live: every
|
||||
# alternative below is the second, HCFA half of each pair; the first half is
|
||||
# the UB form's, unchanged. Within one row only one half of a pair is ever
|
||||
# present, so `.first` still resolves to the single real match either way.
|
||||
SEL_CPT_CODE = ('[id^="uBClaimCPTCode"], [data-fieldname="claimCPTCode"], '
|
||||
'[data-fieldname="web_cptcode"]')
|
||||
SEL_CPT_POS = '[id^="uBClaimPOS"], [data-fieldname="ClaimCPTPOS"]'
|
||||
SEL_CPT_MOD1 = '[id^="uBClaimMod1"], [data-fieldname="ClaimCPTMOD1"]'
|
||||
SEL_CPT_MOD2 = '[id^="uBClaimMod2"], [data-fieldname="ClaimCPTMOD2"]'
|
||||
SEL_CPT_MOD3 = '[id^="uBClaimMod3"], [data-fieldname="ClaimCPTMOD3"]'
|
||||
SEL_CPT_MOD4 = '[id^="uBClaimMod4"], [data-fieldname="ClaimCPTMOD4"]'
|
||||
# In M1-M4 order — what condition #12 (bh_h_code_modifier.py) clears.
|
||||
SEL_CPT_MOD = (SEL_CPT_MOD1, SEL_CPT_MOD2, SEL_CPT_MOD3, SEL_CPT_MOD4)
|
||||
SEL_CPT_ICD1 = ('[id^="uBClaimICD1"], [data-fieldname="ClaimCPTICD1"], '
|
||||
'[data-fieldname="web_icd1"]')
|
||||
SEL_CPT_ICD2 = ('[id^="uBClaimICD2"], [data-fieldname="ClaimCPTICD2"], '
|
||||
'[data-fieldname="web_icd2"]')
|
||||
SEL_CPT_ICD3 = ('[id^="uBClaimICD3"], [data-fieldname="ClaimCPTICD3"], '
|
||||
'[data-fieldname="web_icd3"]')
|
||||
SEL_CPT_ICD4 = ('[id^="uBClaimICD4"], [data-fieldname="ClaimCPTICD4"], '
|
||||
'[data-fieldname="web_icd4"]')
|
||||
# In ICD1-4 order — the order a CPT row's diagnosis-pointer list is read and
|
||||
# rewritten in, condition #6 (depression_dx_pointer.py).
|
||||
SEL_CPT_ICD = (SEL_CPT_ICD1, SEL_CPT_ICD2, SEL_CPT_ICD3, SEL_CPT_ICD4)
|
||||
|
||||
# The CPT row's own "Assign To Patient" checkbox — what condition #6 needs
|
||||
# to re-check on a G8510/G8431 row that was previously excluded from the
|
||||
# claim. Matched on ng-click, timestamp-free like the CPT/HCPCS tab links,
|
||||
# rather than the id (`claimCPTBillToInsChk<timestamp><row>`, verified live
|
||||
# on the HCFA screen, claim 9103000). Not yet independently confirmed on
|
||||
# the UB form — `ensure_cpt_rows_checked()` treats a row with none of these
|
||||
# as nothing to do rather than a failure, since this only ever adds a
|
||||
# check and never removes one.
|
||||
SEL_CPT_ASSIGN_TO_PATIENT = '[ng-click*="updateBillToIns("]'
|
||||
|
||||
# The row's own Billed Fee — combined UB-id-prefix + HCFA-data-fieldname
|
||||
# pattern, same shape as the ICD1-4 selectors above. Needed by condition
|
||||
# #22's "change the fee amount as Zero" half (medicare_telehealth.py).
|
||||
# Dental: `web_billedFee`, whose blur runs eCW's editCPTRow('BILLED_FEE').
|
||||
SEL_CPT_BILLED_FEE = ('[id^="uBClaimBillFee"], '
|
||||
'[data-fieldname="ClaimCPTBilledFee"], '
|
||||
'[data-fieldname="web_billedFee"]')
|
||||
|
||||
# The row's own Units — same UB-id-prefix + HCFA-data-fieldname pair as its
|
||||
# neighbours (both halves are the ones extract()'s own HCFA_FIELDNAME map
|
||||
# already reads). Needed by condition #18 (vaccine_admin_codes.py) to set the
|
||||
# "each additional" admin code's Units to the vaccine count - 1. It carries
|
||||
# `ng-blur="padZero($index);editCPTRow('UNITS')"` — that blur is what makes
|
||||
# eCW re-price the line, so a write here must blur, the same as M1.
|
||||
SEL_CPT_UNITS = ('[id^="uBClaimUnits"], [data-fieldname="ClaimCPTUnits"], '
|
||||
'[data-fieldname="web_units"]')
|
||||
|
||||
# Adding a CPT/HCPCS line item is NOT done by typing into the grid's
|
||||
# trailing "*" row — that field is for something else. The real flow,
|
||||
# verified against a live "Claim" screen (claim 9109000): click "Add" to
|
||||
# open eCW's shared Fee Schedule picker, search it by code, select the
|
||||
# matching row, and click the picker's own OK.
|
||||
#
|
||||
# "Add" itself is timestamped like everything else on the claim screen
|
||||
# (`claimAddCPTBtn1789730090463` there, `addClaimProcCode1789729056266` on
|
||||
# the Dental Claim screen), so it is matched on its shared, timestamp-free
|
||||
# ng-click instead — the same reasoning as SEL_CANCEL_HCFA.
|
||||
SEL_CPT_ADD_BTN = 'button[ng-click*="addProcedureCode("]'
|
||||
|
||||
# The Fee Schedule picker is a shared, reusable eCW component: unlike
|
||||
# everything else on the claim screen, ITS OWN controls carry stable ids
|
||||
# with no per-open timestamp at all — verified live (claim 9109000).
|
||||
SEL_FEE_SCHEDULE_CPT_SEARCH = "#claimFeeScheduleIpt6" # ng-model="claimFS.cptcode"
|
||||
SEL_FEE_SCHEDULE_RESULTS = ".claimFSCPTTbl" # ng-repeat="obj in claimFS.procedures"
|
||||
SEL_FEE_SCHEDULE_OK = "#claimFeeSchOkBtn" # ng-click="addClaimFSCPTCode('save');addCptClaimEdit();"
|
||||
SEL_FEE_SCHEDULE_CANCEL = "#claimFeeScheduleBtn8" # ng-click="addClaimFSCPTCode('cancel');"
|
||||
|
||||
|
||||
# ── Progress Notes ────────────────────────────────────────────────────────────
|
||||
# `ng-click="viewProgNote()"`, and `ng-disabled="!FormData.EnableProgressNote"` —
|
||||
# so it can be present and disabled, and clicking it then does nothing at all.
|
||||
# claim_form.open_progress_notes() checks enabled, not just visible.
|
||||
#
|
||||
# Matched on ng-click, not the UB form's stable `#UBClaimProgressNoteBtn` id:
|
||||
# verified against a live HCFA/professional claim that this same ng-click
|
||||
# sits on a DIFFERENT, per-open-timestamped id there
|
||||
# (`claimProgressNoteBtn<timestamp>`) — the id is not shared across screens,
|
||||
# but the click handler is, so this is what makes Prog. Notes work on both.
|
||||
# Fallback (core/fallback.py): the ids described above — UB's stable one
|
||||
# and the HCFA screen's timestamped one.
|
||||
PROGRESS_NOTES_CHAIN = ('button[ng-click="viewProgNote()"]',
|
||||
"#UBClaimProgressNoteBtn",
|
||||
'button[id^="claimProgressNoteBtn"]')
|
||||
SEL_PROGRESS_NOTES = ", ".join(PROGRESS_NOTES_CHAIN)
|
||||
|
||||
# The professional/HCFA "Claim" screen's own OK/Save — verified against a
|
||||
# live claim: `id="claimScreenOkBtn<timestamp>"`,
|
||||
# `ng-click="currentButtonAction='';saveAllData()"` (no `true` argument,
|
||||
# unlike the UB form's `saveAllData(true)`). Matched on the substring both
|
||||
# share rather than the id, which is timestamped here. Used only as a
|
||||
# fallback in save_claim(), after the UB-specific #UBClaimBtn18 — this is
|
||||
# the one place both screens' write paths actually diverge in shape, since
|
||||
# everything else about "close the form" already has a shared or per-screen
|
||||
# selector.
|
||||
SEL_OK_HCFA = 'button[ng-click*="saveAllData("]'
|
||||
|
||||
|
||||
# ── Tables read during extraction ─────────────────────────────────────────────
|
||||
# Stable ids, all of them verified against the live DOM on 2026-09-07 — UB
|
||||
# form only, unlike SEL_TBL_ICD below.
|
||||
SEL_TBL_INSURANCE = "#UBClaimTbl3" # InvInsList
|
||||
SEL_TBL_PAYMENTS = "#UBClaimTbl4" # PayAdjRefund
|
||||
SEL_TBL_REVENUE = "#UBClaimTbl10" # UB92CPTDetails
|
||||
SEL_TBL_ERRORS = "#UBClaimTbl17" # ChkCurUB92Batch
|
||||
|
||||
# Matched by id PREFIX, not the full id: the UB form's own icdCodesTable
|
||||
# carries no timestamp (verified 2026-09-07), but the professional/HCFA
|
||||
# screen's copy of the SAME id is timestamped (`icdCodesTable1789643824545`,
|
||||
# claim 9103000) — a plain prefix match reaches both without needing a
|
||||
# second, HCFA-only selector the way the CPT row fields did.
|
||||
SEL_TBL_ICD = '[id^="icdCodesTable"]' # ICDCodes
|
||||
|
||||
# ── Progress Notes viewer ─────────────────────────────────────────────────────
|
||||
# Close is matched on its ng-click, and **the typo is eCW's own**:
|
||||
# `closeProgressNoteViwer()` — Viwer, not Viewer. Correcting it here would match
|
||||
# nothing. Left exactly as shipped, with this note so nobody "fixes" it.
|
||||
SEL_PN_CLOSE = 'button[ng-click="closeProgressNoteViwer()"]'
|
||||
# The viewer's own iframe, from the live DOM (run 2026-10-12): it is there
|
||||
# — blank — while the note document is still loading, before the viewer's
|
||||
# Close button is drawn. While it is on screen the first Prog. Notes click
|
||||
# DID take, so it must never be clicked again (that stacked a second viewer
|
||||
# over the first; see claim_form._wait_for_note).
|
||||
SEL_PN_VIEWER_FRAME = ('iframe#ProgNoteViwerFrame, '
|
||||
'iframe[name="ProgNoteViwerFrame"]')
|
||||
|
||||
# The progress note is a whole printable document with its own <html>, so it
|
||||
# arrives in an iframe or a separate tab rather than inline. These are how a
|
||||
# candidate frame/tab is recognised as holding the note:
|
||||
# #mainThead the repeating patient header row
|
||||
# [prisma-section] eCW tags every section of the note with one of these
|
||||
SEL_PN_MARKER = "#mainThead, [prisma-section]"
|
||||
SEL_PN_SECTION = "[prisma-section]"
|
||||
SEL_PN_HEADER = "#mainThead"
|
||||
|
||||
# Hidden span carrying "true" when the chart is locked. Read with textContent,
|
||||
# never innerText: it is display:none, and innerText returns "" for that.
|
||||
SEL_PN_LOCKED = "#chartLockedId"
|
||||
|
||||
# ── UB Condition Codes (FL 18-28) ────────────────────────────────────────────
|
||||
# Mapped from Dom.txt (a UB form, 2026-10-16): eleven `select-claim-code`
|
||||
# inputs, ids uBClaimCondtnCode1..11 (`search-record="Ub92Data.ConditionCode
|
||||
# N"`), 2 characters each. Plain text inputs: ng-focus loads the code list,
|
||||
# ng-keyup shows it; no ng-blur / ng-change. Format with n = 1..11. The UB
|
||||
# form only — the HCFA screen's are not mapped, and need not be: Medicare is
|
||||
# billed on UB (document #2; operator, 2026-10-21). On the
|
||||
# form ON SCREEN only: eCW leaves closed forms in the DOM (review,
|
||||
# 2026-10-19). claim_form._EXTRACT_JS reads the same eleven boxes (its own
|
||||
# copy, from the shown #UBClaimDiv) — change both together.
|
||||
SEL_UB_CONDITION_CODE = '#UBClaimDiv:visible [id="uBClaimCondtnCode{n}"]'
|
||||
UB_CONDITION_CODE_BOXES = 11
|
||||
|
||||
# ── Patient Info → Sliding Fee Schedule (read-only expiry check) ─────────────
|
||||
# Mapped from Dom.txt (UB claim 9124000, 2026-09-30). The claim header's
|
||||
# "Info" button carries a timestamped id (claimPatientInfo<ts>), so it is
|
||||
# matched on its ng-click instead.
|
||||
SEL_CLAIM_PATIENT_INFO = "button[ng-click=\"loadPatientHubnInfo('Info');\"]"
|
||||
SEL_PT_INFO_MODAL = "#ptInfo"
|
||||
# Patient Information's own Cancel (bottom right). Never its Save.
|
||||
SEL_PT_INFO_CANCEL = 'button[ng-click="cancelbtn()"]'
|
||||
# The "You have unsaved data" prompt Cancel can raise: its "Don't Save".
|
||||
SEL_PT_INFO_DONT_SAVE = "#savePrompt-tplBtn4"
|
||||
# ng-disabled="slidingfeedisable"; green (useGreenClass) when one is assigned.
|
||||
SEL_SLIDING_FEE_BTN = "#btnSlidingFeeschedule"
|
||||
SEL_SLIDING_FEE_MODAL = "#slidingfeescheduleDailog" # eCW's spelling
|
||||
SEL_SLIDING_FEE_CLOSE = (f'{SEL_SLIDING_FEE_MODAL} '
|
||||
'button[ng-click="closeSlidingFeeScheduleModal()"]')
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
The Claim Lookup screen (`claimLookup.jsp`) — filters, results grid, export.
|
||||
|
||||
The single most expensive lesson on this screen: **eCW reuses ids.** Both the
|
||||
CSV and the EXCEL buttons are `#claimLookupBtn1002`, so `.first` on that id
|
||||
exports CSV while the log looks perfectly clean. Anywhere an id is not unique,
|
||||
the selector below matches on `ng-click` or on visible text instead, and says
|
||||
why.
|
||||
"""
|
||||
|
||||
# ── Date type ─────────────────────────────────────────────────────────────────
|
||||
# The dropdown's trigger. Matched on ng-bind rather than the class, because
|
||||
# `.toggledropdown` appears several times on this screen — the claim-no type
|
||||
# next to it is another one. Its own text IS the current value
|
||||
# (ng-bind="selectedDateType"), which makes it the thing to assert on after a
|
||||
# selection instead of guessing at some other confirmation.
|
||||
SEL_DATE_TYPE_TOGGLE = "span.toggledropdown[ng-bind='selectedDateType']"
|
||||
|
||||
# The options list. Text-matched inside it, never by the ngR<N> ids: those are
|
||||
# generated from the ngRepeat index, so `claimLookupLink2ngR1` only means
|
||||
# "Claim Dt(s)" for as long as eCW keeps the list in this exact order.
|
||||
SEL_DATE_TYPE_MENU = "#claimLookupUl2"
|
||||
|
||||
# Option labels. Here rather than in settings because they are DOM text — what
|
||||
# the option actually reads — not a preference.
|
||||
DATE_TYPE_CLAIM = "Claim Dt(s)"
|
||||
DATE_TYPE_SERVICE = "Service Dt(s)"
|
||||
|
||||
|
||||
# ── Claim Status ──────────────────────────────────────────────────────────────
|
||||
# A plain <select>, so no dropdown dance. Two things hang off it: the balance
|
||||
# row's enableBalanceFlag (via ng-change="enableInsurance()"), and whether a
|
||||
# claim-based date type can match anything at all.
|
||||
SEL_CLAIM_STATUS = "#claimStatusCodeId"
|
||||
CLAIM_STATUS_NO_CLAIM = "EncMissingClaims" # the default; has no claim date
|
||||
|
||||
# The mode selector beside it (Claim Status / User - (PRS) / PRS / …). Left
|
||||
# alone, but #claimStatusCodeId only exists while this is on "Claim Status"
|
||||
# (ng-show="ClaimStatusFlag"), so a missing select means the mode moved.
|
||||
SEL_CLAIM_STATUS_MODE = "span.toggledropdown[ng-bind='selectedClaimStatus']"
|
||||
|
||||
|
||||
# ── Date range ────────────────────────────────────────────────────────────────
|
||||
# Both carry jQuery UI's `hasDatepicker`, so focusing one opens a calendar
|
||||
# overlay that can sit on top of the next control.
|
||||
SEL_DATE_FROM = "#fromdate"
|
||||
SEL_DATE_TO = "#todate"
|
||||
|
||||
# jQuery UI parks one shared calendar here. Escape alone does not close it —
|
||||
# observed 2026-09-02 still open over the results grid at Lookup time — so
|
||||
# claim_filters.py blurs the field and checks this went away.
|
||||
SEL_DATEPICKER = "#ui-datepicker-div"
|
||||
|
||||
|
||||
# ── Balance filter ────────────────────────────────────────────────────────────
|
||||
# Five controls, not two: operator, value, And/Or, second operator, second
|
||||
# value. All of them carry ng-disabled="!enableBalanceFlag", which eCW drives
|
||||
# off the Claim Status selection — so they are often disabled and there is
|
||||
# nothing to type into.
|
||||
SEL_BAL_OP1 = "#claimLookupSel6"
|
||||
SEL_BAL_VAL1 = "#claimLookupIpt11"
|
||||
SEL_BAL_ANDOR = "#claimLookupSel7"
|
||||
SEL_BAL_OP2 = "#claimLookupSel8"
|
||||
SEL_BAL_VAL2 = "#claimLookupIpt12"
|
||||
|
||||
# The ng-model names behind the five controls above, in the same order.
|
||||
#
|
||||
# Needed because the row arrives holding "> 0.0" while every control is
|
||||
# disabled: nothing can be clicked or typed, yet eCW builds the query from the
|
||||
# scope values, not from the inputs — so the filter is live and unreachable.
|
||||
# Writing the scope is the only way to actually clear it.
|
||||
BAL_MODELS = ("_condition1", "_conditionVal1", "andOrCondition",
|
||||
"_condition2", "_conditionVal2")
|
||||
|
||||
|
||||
# ── Claim No ──────────────────────────────────────────────────────────────────
|
||||
# The single-claim search box. `ng-model="_InvId"`, so it must be written with
|
||||
# `fill()`: that dispatches the `input` event ng-model listens for, and eCW
|
||||
# builds its query from the scope, not from the DOM. Setting `.value` through
|
||||
# `evaluate` would show the right number on screen and search for nothing.
|
||||
#
|
||||
# `autocomplete="one-time-code"` on it is eCW's doing, not a hint about the
|
||||
# field — it is how they stop the browser offering past claim numbers.
|
||||
# Fallback (core/fallback.py): its ng-model, which is what eCW reads.
|
||||
CLAIM_NO_CHAIN = ("#claimLookupIpt10", 'input[ng-model="_InvId"]')
|
||||
SEL_CLAIM_NO = ", ".join(CLAIM_NO_CHAIN)
|
||||
|
||||
# The type selector beside it decides what that box means. eCW switches the
|
||||
# date-type dropdown's pointer-events off while this reads "Rule Engine ID#",
|
||||
# which is the one observable sign that the box is not searching claim numbers.
|
||||
# No selector for it has been captured, so nothing here reads it directly —
|
||||
# `claim_results.search_claim` verifies the *result* instead, by checking that
|
||||
# the claim id it typed actually comes back in the grid.
|
||||
|
||||
|
||||
# ── Search ────────────────────────────────────────────────────────────────────
|
||||
# ng-click="getClaimsListFromLookup('lookup')". Distinct from
|
||||
# #btnclaimlookupClear right beside it, which wipes the whole form.
|
||||
# Fallback (core/fallback.py): that ng-click — not the Clear button's.
|
||||
LOOKUP_BTN_CHAIN = ("#btnclaimlookup",
|
||||
"[ng-click=\"getClaimsListFromLookup('lookup')\"]")
|
||||
SEL_LOOKUP_BTN = ", ".join(LOOKUP_BTN_CHAIN)
|
||||
|
||||
|
||||
# ── Export: Billing → View Claims Report → EXCEL ──────────────────────────────
|
||||
# The toolbar above the results grid. Note there are TWO "Billing" controls on
|
||||
# this screen — the left-nav item pages/leftnav.py uses, and this one.
|
||||
SEL_BILLING_BTN = "#claimLookupBtn17"
|
||||
SEL_BILLING_MENU = "#claimLookupUl10"
|
||||
|
||||
# "View Claims Report" opens a Bootstrap `dropdown-submenu`, which normally
|
||||
# reveals on hover rather than click — the button carries no ng-click at all.
|
||||
# claim_report.py hovers first and only clicks if hovering does not open it.
|
||||
SEL_VIEW_REPORT = "#claimLookupBtn71"
|
||||
SEL_REPORT_MENU = "#claimLookupUl1000"
|
||||
|
||||
|
||||
def report_item(fmt: str) -> str:
|
||||
"""
|
||||
The format button inside the report submenu.
|
||||
|
||||
Matched on ng-click and deliberately **not** on the button id: eCW gives CSV
|
||||
and EXCEL the *same* id, `#claimLookupBtn1002`. Clicking that id with
|
||||
`.first` exports CSV — a file downloads, the run looks clean, and the format
|
||||
is wrong. `mnuViewReport_Click('EXCEL');` is unique per format.
|
||||
"""
|
||||
return f'{SEL_REPORT_MENU} button[ng-click*="{fmt}"]'
|
||||
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
The fallback chains `core/fallback.report()` checks (2026-10-21): name →
|
||||
(selectors module, chain), the chain's first entry the primary. Each chain
|
||||
is defined beside its selector, which is the chain joined as a CSS list.
|
||||
"""
|
||||
|
||||
from . import claim_form as form_sel
|
||||
from . import claim_lookup as lookup_sel
|
||||
from . import login as login_sel
|
||||
|
||||
CHAINS = {
|
||||
"Password field": ("login", login_sel.PASSWORD_CHAIN),
|
||||
"Log In button": ("login", login_sel.LOGIN_BTN_CHAIN),
|
||||
"Claim No box": ("claim_lookup", lookup_sel.CLAIM_NO_CHAIN),
|
||||
"Lookup button": ("claim_lookup", lookup_sel.LOOKUP_BTN_CHAIN),
|
||||
"UB Cancel button": ("claim_form", form_sel.CANCEL_CHAIN),
|
||||
"Dental Cancel button": ("claim_form", form_sel.CANCEL_DENTAL_CHAIN),
|
||||
"Prog. Notes button": ("claim_form", form_sel.PROGRESS_NOTES_CHAIN),
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Login, captcha and second-factor selectors (eCW V12).
|
||||
|
||||
Several of these are comma-separated selector *lists* rather than single ids.
|
||||
That is not laziness: eCW gives these controls no stable id across builds, so a
|
||||
label match with id fallbacks is what actually survives an upgrade. The ones
|
||||
written as a bare id are the ones observed to be stable.
|
||||
"""
|
||||
|
||||
SEL_USERNAME = "#doctorID"
|
||||
SEL_NEXT = "#nextStep"
|
||||
# Fallback chains (core/fallback.py, 2026-10-21): the primary first, then
|
||||
# what the live login page showed — the field is `type="password"`, the
|
||||
# button `<input type="button" value="Log In" class="btn btn-emr btn-sm
|
||||
# agentButton">` (network log of 2026-10-20).
|
||||
PASSWORD_CHAIN = ("#passwordField", 'input[type="password"]')
|
||||
SEL_PASSWORD = ", ".join(PASSWORD_CHAIN)
|
||||
LOGIN_BTN_CHAIN = ("#Login", 'input[type="button"][value="Log In" i]')
|
||||
SEL_LOGIN_BTN = ", ".join(LOGIN_BTN_CHAIN)
|
||||
SEL_OTP_INPUT = "#OTPCode"
|
||||
SEL_OTP_SUBMIT = "#Submit"
|
||||
SEL_HOME = "#jellybean-panelLink4"
|
||||
SEL_USERNAME_ERR = "#err"
|
||||
SEL_ALT_TFA = "#altTFAMsg"
|
||||
SEL_ALT_TFA_SEND = "#altTFASend"
|
||||
|
||||
# "Resend OTP" on the push-notification screen.
|
||||
#
|
||||
# The real id is **#resendOtpBtn** — `<button id="resendOtpBtn"
|
||||
# onclick="resendPushNT();">Resend OTP</button>`. That is why the resend branch
|
||||
# failed on 2026-09-04 with `Locator.click: Timeout 30000ms exceeded`: the list
|
||||
# here guessed `#resendOTP`, `#ResendOTP` and `#resend`, and eCW spells it with
|
||||
# a lowercase `tp` and a `Btn` suffix. The guesses are kept behind the real id
|
||||
# as a hedge against a rename, not because any of them has ever matched.
|
||||
#
|
||||
# It is often `disabled="disabled"`, so visibility is not enough to click it:
|
||||
# eCW disables it on the timed-out page, and `setReSendOtpMsg()` disables it for
|
||||
# good once `leftOtpCount` hits 0 — three resends per login, not unlimited.
|
||||
SEL_RESEND_OTP = (
|
||||
"#resendOtpBtn, "
|
||||
"input[value*='Resend' i], button:has-text('Resend'), "
|
||||
"a:has-text('Resend'), #resendOTP, #ResendOTP, #resend"
|
||||
)
|
||||
|
||||
# The status line on the 2FA screen. **Read its text, never just its
|
||||
# visibility.** eCW reuses this one element for the "a notification has been
|
||||
# sent to your email" notice AND for every dead end — so treating it as "the
|
||||
# push notice is showing" makes a timed-out login look like a healthy one that
|
||||
# is still waiting for mail.
|
||||
SEL_PUSH_NOTICE = "#dispMessage"
|
||||
SEL_OTP_MESSAGE = "#dispMessage"
|
||||
|
||||
# "Try Again" — `onclick="logoutSeamLessSSO()"`, which navigates to
|
||||
# `/login/authenticate/logOut`. eCW reveals it alongside every dead-end message.
|
||||
# The bot does not click it: a fresh browser is cleaner than a logged-out
|
||||
# session in a browser that has already been through a failed handshake.
|
||||
SEL_TRY_AGAIN = "#backToLoginTryAgain"
|
||||
|
||||
# The three dead ends `initCheckTokenTimer()` can write into #dispMessage. It
|
||||
# polls `checkSecureToken` every second and, on any of these, stops polling —
|
||||
# so the page will never move on and waiting is pointless. Matched on text
|
||||
# because the id is the same for all three and for the healthy notice.
|
||||
#
|
||||
# isTokenExpired → the approval link was not clicked in time
|
||||
# isVerified === "no" → somebody answered "No, it wasn't me"
|
||||
# leftOtpCount <= 0 → resends exhausted (three per login)
|
||||
TXT_LOGIN_TIMED_OUT = "log in request has timed out"
|
||||
TXT_NOT_ME = "weren't trying to Log in"
|
||||
TXT_NO_ATTEMPTS = "No attempt left"
|
||||
|
||||
# Confirm button on the emailed openLoginConfirmation page. eCW gives it no
|
||||
# stable id across builds, so match the label and fall back to generic buttons.
|
||||
SEL_APPROVE_YES = (
|
||||
"button:has-text(\"it's me\"), input[value*=\"it's me\" i], "
|
||||
"a:has-text(\"it's me\"), "
|
||||
"#yesItsMe, #btnYes, #yes, "
|
||||
"button:has-text('Yes'), input[type='button'][value*='Yes' i], "
|
||||
"input[type='submit'][value*='Yes' i]"
|
||||
)
|
||||
|
||||
SEL_CAPTCHA_INPUT = "#captchaVal, #captchaVal1, #captchaField, #txtCaptcha"
|
||||
SEL_CAPTCHA_IMG = (
|
||||
"img#imageCaptcha, img#captchaImage, img#captchaImg, "
|
||||
"img[id*='captcha']:not([id*='security']):not([id*='landmark'])"
|
||||
)
|
||||
|
||||
|
||||
# eCW's refusal page: "Access to the application has been restricted."
|
||||
#
|
||||
# Measured 2026-09-01: this is an IP check, not a rate limit. The same approval
|
||||
# link answers HTTP 400 unproxied and HTTP 200 through the proxy, from the same
|
||||
# machine, seconds apart. So for the bot this page almost always means "the
|
||||
# browser is not actually going through the proxy" — not "the account is locked".
|
||||
# It is what makes the manual flow impossible: the webmail host is blocked BY
|
||||
# the proxy, and the portal is blocked WITHOUT it, so a human has to toggle.
|
||||
#
|
||||
# Detecting it explicitly matters because the portal keeps rendering the normal
|
||||
# 2FA screen afterwards and still claims it emailed an approval link while
|
||||
# silently sending nothing — otherwise the bot polls an empty mailbox for two
|
||||
# minutes and blames the mailbox.
|
||||
TXT_RESTRICTED = "has been restricted"
|
||||
|
||||
# eCW's post-login "This password expires in 14 days. Do you want to change
|
||||
# the password now?" box, with OK (= go and change it now) and Cancel (= not
|
||||
# now). Seen live on 2026-10-05: it sat over the grey page after the 2FA
|
||||
# approval, the home screen never came up, and both runs that day reported
|
||||
# "Login not confirmed". Its DOM has not been captured yet, so it is found by
|
||||
# its TEXT, and only a button labelled exactly "Cancel" inside the same box is
|
||||
# ever clicked — never OK, which opens the change-password screen.
|
||||
TXT_PASSWORD_EXPIRES = r"password\s+expires\s+in\s+(\d+)\s+day"
|
||||
# A password that has already expired cannot be waved away with Cancel: eCW
|
||||
# makes the user set a new one, which the bot must never do.
|
||||
TXT_PASSWORD_EXPIRED = r"password\s+(?:has\s+)?expired"
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
The eCW application shell: left navigation panel and the route spinner.
|
||||
|
||||
Three facts from the live DOM shape every locator built on these, and all three
|
||||
cost a debugging session to find:
|
||||
|
||||
* `#JellyBeanCountCntrl` renders **twice**, so `SEL_NAV_TOGGLE` matches two
|
||||
elements and Playwright's strict mode errors out without `.first`.
|
||||
* the toggle's handler `launchLeftPanel()` is a **toggle**, not an "open" —
|
||||
clicking it on an already-open panel closes it. State first, click second.
|
||||
* the panel's contents are injected by `getleftnavhtml()` on
|
||||
`DOMContentLoaded`, so `SEL_MENU_BILLING` does not exist at the moment
|
||||
login is confirmed. Wait for it; do not assume it.
|
||||
"""
|
||||
|
||||
SEL_NAV_TOGGLE = "#jellybean-panelLink4"
|
||||
|
||||
# launchLeftPanel() flips the `visible` class on .response-nav-lp (#leftnavApp).
|
||||
SEL_NAV_PANEL = "#leftnavApp"
|
||||
SEL_NAV_OPEN = "#leftnavApp.visible"
|
||||
|
||||
SEL_MENU_BILLING = "#Billing_menu > a"
|
||||
SEL_MENU_BILLING_OPEN = "li#Billing_menu.open"
|
||||
|
||||
# Click the LABEL span, not the anchor: the anchor also holds #favorite_16,
|
||||
# whose own ng-click stars the menu item instead of navigating.
|
||||
SEL_CLAIMS = "#CLAIMS_BILLING"
|
||||
SEL_CLAIMS_LABEL = "#CLAIMS_BILLING span.padl10"
|
||||
|
||||
# What that item's ng-click openURL() actually does. Kept here because it is
|
||||
# also the fallback route when the menu markup shifts.
|
||||
CLAIMS_HASH = "#/mobiledoc/jsp/webemr/webpm/claimLookup.jsp"
|
||||
|
||||
# Angular's route spinner — hidden once the requested view has rendered.
|
||||
SEL_ROUTE_LOADER = "#routeLoader"
|
||||
Reference in new issue
Block a user