"""Browser validation for the invoicing QuickBooks customer modal flow.""" from __future__ import annotations import json import os import sys import time from pathlib import Path from playwright.sync_api import sync_playwright BASE = "http://localhost:4000" SCREENSHOT_DIR = Path("/tmp/invoicing_validation") SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True) # Recorded network traffic requests_log: list[dict] = [] def main() -> int: with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(viewport={"width": 1440, "height": 900}) page = context.new_page() def on_request(req): url = req.url if ( "/api/invoicing/customers" in url or "wfdash" in url.lower() or "quickbooks" in url.lower() or "qb" in url.lower() or "/api/" in url and "pocketbase" not in url.lower() ): try: body = req.post_data except Exception: body = None requests_log.append({ "method": req.method, "url": url, "headers": dict(req.headers), "body": body, }) print(f"[REQ] {req.method} {url}") if body: print(f" body: {body[:500]}") def on_response(res): url = res.url if ( "/api/invoicing/customers" in url or "wfdash" in url.lower() or "quickbooks" in url.lower() or "qb" in url.lower() ): print(f"[RES] {res.status} {url}") for r in requests_log: if r["url"] == url and "response_status" not in r: try: txt = res.text() except Exception: txt = "" r["response_status"] = res.status r["response_body"] = txt[:1500] break page.on("request", on_request) page.on("response", on_response) # 0. Register a user so we can pass the auth gate print("\n=== STEP 0: Register a dev user ===") page.goto(f"{BASE}/login", wait_until="domcontentloaded") page.wait_for_timeout(1000) # Click the Register tab register_tab = page.get_by_role("tab", name="Register") if register_tab.count() > 0: register_tab.click() page.wait_for_timeout(500) # Fill register form page.fill("#reg-name", "Test Operator") page.fill("#reg-email", "test-operator@example.com") page.fill("#reg-password", "testpass1234") page.fill("#reg-confirm", "testpass1234") page.get_by_role("button", name="Create Account").click() page.wait_for_timeout(3000) page.screenshot(path=str(SCREENSHOT_DIR / "00_register.png"), full_page=True) print(f" current URL: {page.url}") # 1. Open the invoicing page print("\n=== STEP 1: Load invoicing page ===") requests_log.clear() page.goto(f"{BASE}/invoicing", wait_until="domcontentloaded") page.wait_for_timeout(3000) # If we got redirected to login, sign in if "/login" in page.url: print(" redirected to login — signing in") page.fill("#login-email", "test-operator@example.com") page.fill("#login-password", "testpass1234") page.get_by_role("button", name="Sign In").click() page.wait_for_timeout(3000) page.goto(f"{BASE}/invoicing", wait_until="domcontentloaded") page.wait_for_timeout(3000) page.screenshot(path=str(SCREENSHOT_DIR / "01_loaded.png"), full_page=True) print(f" current URL: {page.url}") # Check for QB-ready groups invoice_buttons = page.get_by_role("button", name="Invoice in QuickBooks").all() print(f" 'Invoice in QuickBooks' single buttons found: {len(invoice_buttons)}") bulk_button = page.get_by_role("button", name="Invoice in QuickBooks (bulk)") bulk_visible = bulk_button.count() > 0 and bulk_button.is_visible() bulk_enabled = bulk_visible and bulk_button.is_enabled() print(f" bulk button visible: {bulk_visible}, enabled: {bulk_enabled}") # If toggle is "Ready only" and nothing there, try "All" if len(invoice_buttons) == 0 and bulk_visible and not bulk_enabled: all_toggle = page.get_by_role("radio", name="All") if all_toggle.count() > 0: print(" no Ready groups; switching to 'All'") all_toggle.click() page.wait_for_timeout(1500) page.screenshot(path=str(SCREENSHOT_DIR / "01b_all_toggle.png"), full_page=True) invoice_buttons = page.get_by_role("button", name="Invoice in QuickBooks").all() bulk_button = page.get_by_role("button", name="Invoice in QuickBooks (bulk)") bulk_visible = bulk_button.count() > 0 and bulk_button.is_visible() bulk_enabled = bulk_visible and bulk_button.is_enabled() print(f" After 'All': single buttons={len(invoice_buttons)}, bulk visible={bulk_visible} enabled={bulk_enabled}") if len(invoice_buttons) == 0: print("\n!!! No QB-ready groups found in any toggle. Exiting.") browser.close() return 0 # 2. Open single-order modal print("\n=== STEP 2: Open single-order modal ===") requests_log.clear() invoice_buttons[0].click() page.wait_for_timeout(4000) page.screenshot(path=str(SCREENSHOT_DIR / "02_single_modal.png"), full_page=True) # Inspect the customers POST request single_customers_req = None for r in requests_log: if "/api/invoicing/customers" in r["url"] and r["method"] == "POST": single_customers_req = r break if single_customers_req: print(f"\n[CHECK 1a] POST /api/invoicing/customers fired") print(f" status: {single_customers_req.get('response_status')}") try: body = json.loads(single_customers_req.get("body") or "{}") cq = body.get("customer_query", "") bc = body.get("billing_company", "") print(f" customer_query: {cq!r} (non-empty: {bool(cq.strip())})") print(f" billing_company: {bc!r} (non-empty: {bool(str(bc).strip())})") except Exception as e: print(f" body parse error: {e}") resp_body = single_customers_req.get("response_body", "") print(f" response body (first 400): {resp_body[:400]}") else: print("\n[FAIL] No POST /api/invoicing/customers recorded for single modal") # Inspect the select element modal_selects = page.locator('[role="dialog"] select').all() print(f"\n modal select count: {len(modal_selects)}") single_preselect = None if modal_selects: try: opts = modal_selects[0].locator("option").all_text_contents() val = modal_selects[0].input_value() single_preselect = val print(f" select.value={val!r}") print(f" select.options={opts[:15]}{'...' if len(opts) > 15 else ''}") if len(opts) > 1: print(f" [CHECK 3] list rendered ({len(opts)} entries), pre-selected: {val!r}") else: print(f" [CHECK 3] WARN: only {len(opts)} options in list") except Exception as e: print(f" select error: {e}") # 3. Operator override print("\n=== STEP 3: Operator override pre-selection ===") if modal_selects and len(modal_selects[0].locator("option").all()) > 2: opts = modal_selects[0].locator("option").all() current = modal_selects[0].input_value() new_value = None for o in opts: v = o.get_attribute("value") or "" if v and v != current and v != "": new_value = v break if new_value: modal_selects[0].select_option(new_value) page.wait_for_timeout(500) after = modal_selects[0].input_value() print(f" [CHECK 4] pre-select was {current!r}, after override is {after!r}, changed: {after != current}") page.screenshot(path=str(SCREENSHOT_DIR / "03_override.png"), full_page=True) else: print(" [SKIP] No alternate value to pick for override") else: print(" [SKIP] No select with options to test override") # 4. Submit single — capture QB webhook POST print("\n=== STEP 4: Submit single send to QB webhook ===") # Reset selection to pre-selected closest match if modal_selects and single_preselect is not None: modal_selects[0].select_option(single_preselect) page.wait_for_timeout(300) requests_log.clear() send_btn = page.locator('[role="dialog"]').get_by_role("button", name="Send to QuickBooks") if send_btn.count() > 0 and send_btn.is_enabled(): send_btn.click() page.wait_for_timeout(5000) page.screenshot(path=str(SCREENSHOT_DIR / "04_single_submit.png"), full_page=True) else: print(" [WARN] Send button not enabled") # Find the QB webhook POST qb_webhook_reqs = [ r for r in requests_log if r["method"] == "POST" and "/api/invoicing/customers" not in r["url"] ] print(f"\n[CHECK 5/6] QB webhook POSTs detected (single): {len(qb_webhook_reqs)}") for r in qb_webhook_reqs: print(f" {r['method']} {r['url']}") body_preview = (r.get("body") or "")[:500] print(f" body: {body_preview}") print(f" response_status: {r.get('response_status')}") resp = r.get("response_body", "") print(f" response_body: {resp[:400]}") # 5. Close single, open bulk print("\n=== STEP 5: Open bulk modal ===") # Wait for modal to potentially close automatically page.wait_for_timeout(1500) # If a modal is still open, close it still_open_modal = page.locator('[role="dialog"]') if still_open_modal.count() > 0: cancel_btn = still_open_modal.get_by_role("button", name="Cancel") if cancel_btn.count() > 0: cancel_btn.click() page.wait_for_timeout(500) # Find bulk button again (in case DOM changed) bulk_button = page.get_by_role("button", name="Invoice in QuickBooks (bulk)") if bulk_button.count() == 0 or not bulk_button.is_visible(): print(" [WARN] bulk button not visible/present") else: requests_log.clear() bulk_button.click() page.wait_for_timeout(4000) page.screenshot(path=str(SCREENSHOT_DIR / "05_bulk_modal.png"), full_page=True) bulk_modal = page.locator('[role="dialog"]') bulk_selects = bulk_modal.locator("select").all() print(f" bulk select count: {len(bulk_selects)}") bulk_customers_req = None for r in requests_log: if "/api/invoicing/customers" in r["url"] and r["method"] == "POST": bulk_customers_req = r break if bulk_customers_req: print(f"\n[CHECK 1b/4] Bulk POST /api/invoicing/customers fired") print(f" status: {bulk_customers_req.get('response_status')}") try: body = json.loads(bulk_customers_req.get("body") or "{}") cq = body.get("customer_query", "") bc = body.get("billing_company", "") print(f" customer_query: {cq!r} (non-empty: {bool(cq.strip())})") print(f" billing_company: {bc!r} (non-empty: {bool(str(bc).strip())})") except Exception as e: print(f" body parse error: {e}") resp_body = bulk_customers_req.get("response_body", "") print(f" response body (first 400): {resp_body[:400]}") else: print("\n[FAIL] No POST /api/invoicing/customers recorded for bulk modal") # Pre-select inspection print("\n bulk modal pre-selects:") preselected_count = 0 for i, sel in enumerate(bulk_selects): try: v = sel.input_value() opts = sel.locator("option").all_text_contents() if v: preselected_count += 1 print(f" bulk select[{i}].value={v!r} (options={len(opts)})") except Exception as e: print(f" bulk select[{i}] error: {e}") print(f" pre-selected: {preselected_count}/{len(bulk_selects)}") # Operator override on bulk print("\n=== STEP 6: Operator override on bulk modal ===") if bulk_selects and len(bulk_selects[0].locator("option").all()) > 2: opts = bulk_selects[0].locator("option").all() current = bulk_selects[0].input_value() new_value = None for o in opts: v = o.get_attribute("value") or "" if v and v != current and v != "": new_value = v break if new_value: bulk_selects[0].select_option(new_value) page.wait_for_timeout(500) after = bulk_selects[0].input_value() print(f" [CHECK 5] bulk pre-select was {current!r}, after override is {after!r}, changed: {after != current}") page.screenshot(path=str(SCREENSHOT_DIR / "06_bulk_override.png"), full_page=True) # Click "Send all to QuickBooks" print("\n=== STEP 7: Submit bulk send ===") requests_log.clear() send_all_btn = bulk_modal.get_by_role("button", name="Send all to QuickBooks") if send_all_btn.count() > 0 and send_all_btn.is_enabled(): send_all_btn.click() page.wait_for_timeout(6000) page.screenshot(path=str(SCREENSHOT_DIR / "07_bulk_submit.png"), full_page=True) else: print(" [WARN] Send all button not enabled") qb_webhook_reqs = [ r for r in requests_log if r["method"] == "POST" and "/api/invoicing/customers" not in r["url"] ] print(f"\n[CHECK 5/6] QB webhook POSTs detected (bulk): {len(qb_webhook_reqs)}") for r in qb_webhook_reqs: print(f" {r['method']} {r['url']}") body_preview = (r.get("body") or "")[:500] print(f" body: {body_preview}") print(f" response_status: {r.get('response_status')}") resp = r.get("response_body", "") print(f" response_body: {resp[:400]}") # Final dump of the full request log print("\n=== FULL REQUESTS LOG (relevant) ===") for r in requests_log: print(json.dumps({k: (v[:200] if isinstance(v, str) else v) for k, v in r.items()}, indent=2)[:2000]) print("---") browser.close() return 0 if __name__ == "__main__": sys.exit(main())