From 730c41f9b035a47699221aa1dbc40a651295be04 Mon Sep 17 00:00:00 2001 From: ff Date: Sun, 7 Jun 2026 00:28:01 -0400 Subject: [PATCH] fix: DDMA Create claim stale element, tooth dropdown select, longer page waits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit step2: wait for patient list //tbody//tr then 3s stabilize; wait for patient name link to be element_to_be_clickable before reading href; wait for Create claim button to be element_to_be_clickable (visible+enabled) then 3s for React to finish re-rendering. step3: re-find Create claim button fresh each attempt (avoids stale element from React re-render); try selenium click → js events → js.click() in sequence; verify URL changed before declaring success. step4: open tooth dropdown via JS focus (avoids element-not-interactable on click); select the matching tooth number option directly from 1-32 listbox instead of typing characters. step7: find Submit claim button with individual XPaths to avoid NoneType crash. claims-page: use wouter setLocation for URL param cleanup so internal search state stays in sync. Co-Authored-By: Claude Sonnet 4.6 --- apps/Frontend/src/pages/claims-page.tsx | 25 ++- .../selenium_DDMA_claimSubmitWorker.py | 197 +++++++++++++----- 2 files changed, 163 insertions(+), 59 deletions(-) diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index f7215545..fb5e189b 100755 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -11,7 +11,7 @@ import { ClaimForm } from "@/components/claims/claim-form"; import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/hooks/use-auth"; import { apiRequest, queryClient } from "@/lib/queryClient"; -import { useLocation } from "wouter"; +import { useLocation, useSearch } from "wouter"; import { useAppDispatch, useAppSelector } from "@/redux/hooks"; import { setTaskStatus, @@ -203,15 +203,18 @@ export default function ClaimsPage() { } } if (changed) { - window.history.replaceState({}, document.title, url.toString()); + // Use wouter's setLocation so its internal search state updates too + const newRelative = url.pathname + (url.search ? url.search : "") + url.hash; + setWouterLocation(newRelative, { replace: true }); } } catch (e) { - // ignore (URL API or history.replaceState might throw in very old envs) + // ignore } }; // case1: - this params are set by pdf extraction/patient page or either by patient-add-form. then used in claim page here. - const [location] = useLocation(); + const [location, setWouterLocation] = useLocation(); + const search = useSearch(); const { newPatient, mode } = useMemo(() => { const params = new URLSearchParams(window.location.search); @@ -219,7 +222,7 @@ export default function ClaimsPage() { newPatient: params.get("newPatient"), mode: params.get("mode"), // direct | manual | null}; }; - }, [location]); + }, [location, search]); const handleNewClaim = (patientId: number, appointmentId?: number) => { setSelectedPatientId(patientId); @@ -296,6 +299,16 @@ export default function ClaimsPage() { const patientId = appointment?.patientId; if (!cancelled && patientId) { + // Check if chatbot requested auto-submit with a specific insurance + try { + const raw = sessionStorage.getItem("chatbot_claim_prefill"); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed?.autoSubmit && parsed?.siteKey) { + setChatbotAutoSubmitSiteKey(parsed.siteKey); + } + } + } catch {} handleNewClaim(patientId, appointmentId); clearUrlParams(["appointmentId"]); } @@ -314,7 +327,7 @@ export default function ClaimsPage() { return () => { cancelled = true; }; - }, [location, newPatient]); + }, [location, search, newPatient]); // 1. upsert appointment. const handleAppointmentSubmit = async ( diff --git a/apps/SeleniumService/selenium_DDMA_claimSubmitWorker.py b/apps/SeleniumService/selenium_DDMA_claimSubmitWorker.py index b2955aca..c136140a 100644 --- a/apps/SeleniumService/selenium_DDMA_claimSubmitWorker.py +++ b/apps/SeleniumService/selenium_DDMA_claimSubmitWorker.py @@ -341,30 +341,32 @@ class AutomationDDMAClaimSubmit: def step2_open_member_page(self): """Navigate to member detail page — same approach as DDMA eligibility step2.""" try: + # Wait for the results table to appear, then let it stabilize try: - WebDriverWait(self.driver, 10).until( + WebDriverWait(self.driver, 20).until( EC.presence_of_element_located((By.XPATH, "//tbody//tr")) ) - time.sleep(2) + time.sleep(3) # wait for the list to fully stabilize except TimeoutException: print("[DDMA Claim step2] Warning: Results table not found within timeout") - # Find member-details URL from first row — identical to eligibility step2 + # Wait until the patient name link in the first row is clickable detail_url = None - for selector in [ + PATIENT_LINK_XPATHS = [ "(//table//tbody//tr)[1]//td[1]//a", "(//tbody//tr)[1]//a[contains(@href,'member-details')]", "(//tbody//tr)[1]//a[contains(@href,'member')]", "//a[contains(@href,'member-details')]", - ]: + ] + for selector in PATIENT_LINK_XPATHS: try: - link_el = WebDriverWait(self.driver, 5).until( - EC.presence_of_element_located((By.XPATH, selector)) + link_el = WebDriverWait(self.driver, 20).until( + EC.element_to_be_clickable((By.XPATH, selector)) ) href = link_el.get_attribute("href") if href and "member-details" in href: detail_url = href - print(f"[DDMA Claim step2] Found detail URL: {href}") + print(f"[DDMA Claim step2] Patient link is clickable: {href}") break except Exception: continue @@ -384,14 +386,16 @@ class AutomationDDMAClaimSubmit: print(f"[DDMA Claim step2] Warning — URL: {self.driver.current_url}") try: + # Wait until Create claim button is visible AND enabled (not just in the DOM) WebDriverWait(self.driver, 15).until( - EC.presence_of_element_located((By.XPATH, "//button[@aria-label='Create claim']")) + EC.element_to_be_clickable((By.XPATH, "//button[@aria-label='Create claim']")) ) - print("[DDMA Claim step2] 'Create claim' button found") + print("[DDMA Claim step2] 'Create claim' button is clickable") + time.sleep(3) # let React finish any remaining re-renders except TimeoutException: - print("[DDMA Claim step2] Warning: 'Create claim' button not found") + print("[DDMA Claim step2] Warning: 'Create claim' button not clickable in time") + time.sleep(2) - time.sleep(2) return "SUCCESS" except Exception as e: @@ -406,46 +410,92 @@ class AutomationDDMAClaimSubmit: try: print(f"[DDMA Claim step3] Current URL: {self.driver.current_url}") handles_before = set(self.driver.window_handles) + url_before = self.driver.current_url self.driver.execute_script("window.scrollTo(0, 0);") time.sleep(0.5) - # Log all buttons on page for debugging - all_btns = self.driver.find_elements(By.XPATH, "//button") - print(f"[DDMA Claim step3] Buttons on page: {[b.get_attribute('aria-label') or b.text for b in all_btns]}") + # Re-find the button fresh each attempt to avoid stale element references. + # The page may re-render after step2, so we do not cache the element. + CLAIM_BTN_XPATHS = [ + "//button[@aria-label='Create claim']", + "//button[contains(normalize-space(text()),'Create claim')]", + "//button[.//span[contains(text(),'Create claim')]]", + ] - btn = WebDriverWait(self.driver, 10).until( - EC.element_to_be_clickable((By.XPATH, - "//button[@aria-label='Create claim' and @data-react-aria-pressable='true']" - )) - ) - print(f"[DDMA Claim step3] Found 'Create claim' button, displayed={btn.is_displayed()}, enabled={btn.is_enabled()}") + btn = None + for xpath in CLAIM_BTN_XPATHS: + try: + btn = WebDriverWait(self.driver, 8).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + if btn: + print(f"[DDMA Claim step3] Found 'Create claim' via: {xpath}") + break + except Exception: + continue + + if not btn: + all_btns = self.driver.find_elements(By.XPATH, "//button") + print(f"[DDMA Claim step3] Buttons on page: {[b.get_attribute('aria-label') or b.text for b in all_btns]}") + return "ERROR: step3 failed: 'Create claim' button not found" self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn) time.sleep(0.5) - # Try all click methods in sequence until one causes navigation - self.driver.execute_script(""" - var el = arguments[0]; - el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true})); - el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true})); - el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true})); - el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true})); - """, btn) - print("[DDMA Claim step3] Dispatched pointer+click events on 'Create claim'") + # Try direct Selenium click first, fall back to JS events + clicked = False + for attempt, method in enumerate(["selenium", "js_events", "js_click"]): + try: + # Re-find fresh on each attempt to avoid stale reference + for xpath in CLAIM_BTN_XPATHS: + try: + btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + if btn: + break + except Exception: + continue - time.sleep(2) + if method == "selenium": + btn.click() + elif method == "js_events": + self.driver.execute_script(""" + var el = arguments[0]; + el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true})); + el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true})); + el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true})); + el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true})); + """, btn) + else: + self.driver.execute_script("arguments[0].click();", btn) - # Switch to new tab if one opened - handles_after = set(self.driver.window_handles) - new_handles = handles_after - handles_before - if new_handles: - self.driver.switch_to.window(new_handles.pop()) - print(f"[DDMA Claim step3] Switched to new tab") + print(f"[DDMA Claim step3] Clicked via {method} (attempt {attempt+1})") + time.sleep(2) - print(f"[DDMA Claim step3] Post-click URL: {self.driver.current_url}") + # Check if navigation happened (URL changed or new tab opened) + handles_after = set(self.driver.window_handles) + new_handles = handles_after - handles_before + if new_handles: + self.driver.switch_to.window(new_handles.pop()) + print(f"[DDMA Claim step3] Switched to new tab: {self.driver.current_url}") + clicked = True + break + if self.driver.current_url != url_before: + print(f"[DDMA Claim step3] URL changed to: {self.driver.current_url}") + clicked = True + break + print(f"[DDMA Claim step3] URL unchanged after {method}, retrying...") + except Exception as e: + print(f"[DDMA Claim step3] {method} click failed: {e}, retrying...") - # Wait for claim form — just log, don't fail + if not clicked: + page_text = self.driver.execute_script("return document.body.innerText;")[:300] + print(f"[DDMA Claim step3] No navigation after all click attempts — page: {page_text}") + return "ERROR: step3 failed: button clicked but no navigation occurred" + + # Wait for claim form to load try: WebDriverWait(self.driver, 20).until( EC.any_of( @@ -457,7 +507,7 @@ class AutomationDDMAClaimSubmit: )), ) ) - print(f"[DDMA Claim step3] Claim form loaded") + print(f"[DDMA Claim step3] Claim form loaded: {self.driver.current_url}") except TimeoutException: page_text = self.driver.execute_script("return document.body.innerText;")[:400] print(f"[DDMA Claim step3] Claim form not detected — page: {page_text}") @@ -629,7 +679,18 @@ class AutomationDDMAClaimSubmit: proc_inp = proc_inputs[idx] if idx < len(proc_inputs) else proc_inputs[-1] self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", proc_inp) self._fill_combobox(proc_inp, code, f"procedureCode[{idx}]") - time.sleep(0.5) + # Wait for the tooth input on the second row to be present AND interactable + if tooth: + try: + WebDriverWait(self.driver, 8).until( + lambda d: ( + len(d.find_elements(By.XPATH, "//input[@aria-label='Tooth']")) > idx and + d.find_elements(By.XPATH, "//input[@aria-label='Tooth']")[idx].is_enabled() and + d.find_elements(By.XPATH, "//input[@aria-label='Tooth']")[idx].is_displayed() + ) + ) + except Exception: + time.sleep(1) # fallback: just wait a second except Exception as e: print(f"[DDMA Claim step4] Could not fill procedure code: {e}") @@ -638,7 +699,31 @@ class AutomationDDMAClaimSubmit: try: tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']") if idx < len(tooth_inputs): - self._fill_combobox(tooth_inputs[idx], tooth, f"tooth[{idx}]") + ti = tooth_inputs[idx] + self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", ti) + time.sleep(0.3) + # Open the dropdown via JS focus (avoids element-not-interactable on click) + self.driver.execute_script("arguments[0].focus();", ti) + time.sleep(0.5) + # Select the option matching the tooth number from the 1-32 dropdown + listbox_id = ti.get_attribute("aria-controls") or "" + try: + if listbox_id: + option = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((By.XPATH, + f"//*[@id='{listbox_id}']//*[@role='option' and normalize-space(.)='{tooth}']" + )) + ) + else: + option = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((By.XPATH, + f"//*[@role='listbox']//*[@role='option' and normalize-space(.)='{tooth}']" + )) + ) + option.click() + print(f"[DDMA Claim step4] tooth[{idx}]: selected '{tooth}' from dropdown") + except TimeoutException: + print(f"[DDMA Claim step4] tooth[{idx}]: dropdown not found, skipping") time.sleep(0.3) except Exception as e: print(f"[DDMA Claim step4] Could not fill tooth: {e}") @@ -663,7 +748,7 @@ class AutomationDDMAClaimSubmit: except Exception as e: print(f"[DDMA Claim step4] Could not fill quad: {e}") - # ── Surface (free-text combobox — type directly) ────────────── + # ── Surface (type directly then Tab to billed amount) ───────── if surface: try: surface_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Surface']") @@ -673,9 +758,6 @@ class AutomationDDMAClaimSubmit: surf_inp.send_keys(Keys.CONTROL + "a") surf_inp.send_keys(Keys.DELETE) surf_inp.send_keys(surface) - # Dismiss any listbox with Escape so it doesn't block next field - time.sleep(0.3) - surf_inp.send_keys(Keys.ESCAPE) print(f"[DDMA Claim step4] surface[{idx}]: typed '{surface}'") time.sleep(0.2) except Exception as e: @@ -812,13 +894,22 @@ class AutomationDDMAClaimSubmit: time.sleep(0.5) # Click Submit claim button - submit_btn = WebDriverWait(self.driver, 10).until( - EC.element_to_be_clickable((By.XPATH, - "//button[.//span[contains(text(),'Submit claim')]] | " - "//button[contains(normalize-space(text()),'Submit claim')] | " - "//button[@aria-label='Submit claim']" - )) - ) + submit_btn = None + for xpath in [ + "//button[.//span[contains(text(),'Submit claim')]]", + "//button[contains(normalize-space(text()),'Submit claim')]", + "//button[@aria-label='Submit claim']", + ]: + try: + submit_btn = WebDriverWait(self.driver, 8).until( + EC.element_to_be_clickable((By.XPATH, xpath)) + ) + if submit_btn: + break + except Exception: + continue + if not submit_btn: + return "ERROR: step7 failed: Submit claim button not found" self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", submit_btn) time.sleep(0.3) self.driver.execute_script("""