r""" Waiting for the HCFA and Dental forms to finish loading (review item D7). python tests\test_claim_loaded.py Seen live (run_20260930_163006): `open_claim()` returned the moment the HCFA form's Cancel was visible, Angular had not filled the form yet, `extract()` read 0 CPT rows, Prog. Notes read "disabled" — and the claim went to the summary as Pass. `wait_claim_loaded()` waits for the claim number and for the service-line codes to be filled and steady; a timeout is a reason string on the claim's data, not an exception. Local headless chromium against `tests\fixtures\hcfa_claim.html` and `dental_claim.html`, whose opt-in `?load=` / `?pn=` query strings simulate Angular filling the form late (or never, or with the wrong claim). No eCW. Screenshots go to a temp folder, never the real Screenshots\. """ import io import sys import time import pathlib import inspect import tempfile import contextlib ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "src")) FIXTURES = pathlib.Path(__file__).resolve().parent / "fixtures" HCFA = FIXTURES / "hcfa_claim.html" DENTAL = FIXTURES / "dental_claim.html" UB = FIXTURES / "ub_claim.html" from ecw_bot import settings as cfg # noqa: E402 from ecw_bot.pages import claim_form # noqa: E402 from ecw_bot.flows import export_medcor_ready as flow # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn def _go(page, fixture, query=""): page.goto(fixture.as_uri() + (f"?{query}" if query else "")) def _timed(fn, *args, **kwargs): """(result, seconds, captured stdout).""" buf = io.StringIO() started = time.monotonic() with contextlib.redirect_stdout(buf): out = fn(*args, **kwargs) return out, time.monotonic() - started, buf.getvalue() # ── HCFA ────────────────────────────────────────────────────────────────────── @case def hcfa_already_filled_costs_about_half_a_second(page): """The normal claim: the only cost is the 500 ms stability window.""" _go(page, HCFA) out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000") assert out is None, out assert took < 1.5, f"took {took:.1f}s on a form that was already loaded" assert "HCFA form loaded" in log and "3 CPT rows" in log, log @case def hcfa_filled_late_is_waited_for(page): _go(page, HCFA, "load=delayed") # Proves the fixture: right now the form is blank, the way eCW serves it. assert claim_form.extract(page)["cpt"] == [], "fixture is not blank" out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000") assert out is None, out assert took >= 0.7, f"returned after {took:.2f}s — before the form filled" assert len(claim_form.extract(page)["cpt"]) == 3, "read short after wait" @case def hcfa_rows_filled_one_at_a_time_are_waited_for_until_they_hold_still(page): """ The stability rule (D7): the same non-zero count held for 500 ms. Angular fills the rows one digest at a time — here row 1 at 0.8 s, row 2 at 1.1 s, row 3 at 1.4 s. A wait that took the first non-zero count would return at about 0.8-1.0 s and extract() would read 1 or 2 rows; only the 500 ms window keeps it until 1.4 s + 0.5 s. """ _go(page, HCFA, "load=staggered") out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000") assert out is None, out assert took >= 1.9, f"returned after {took:.2f}s — rows still filling" assert len(claim_form.extract(page)["cpt"]) == 3, "read short after wait" assert "3 CPT rows" in log, log @case def hcfa_hidden_leftover_form_of_another_claim_is_not_read(page): """ eCW leaves closed forms in the DOM. A hidden copy of claim 5000002's form sits ahead of this one, and this form's claim number is still hidden while Angular fills it: the old document-wide read fell back to the hidden copy and raised a false "wrong claim" — the claim came back unread. Only the window holding the visible Cancel is read now. """ _go(page, HCFA, "load=delayed&stale=other") out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000") assert out is None, out assert took >= 0.7, f"returned after {took:.2f}s — before the form filled" assert "3 CPT rows" in log, log @case def hcfa_hidden_leftover_rows_do_not_count_as_loaded(page): """The same hidden leftover, showing this claim's number and one filled row, in front of a form that never fills: its row must not make the blank form count as loaded (the old read returned None here).""" _go(page, HCFA, "load=never&stale=same") out, _, _ = _timed(claim_form.wait_claim_loaded, page, "9103000", timeout_ms=1_000) assert out == ("HCFA form did not finish loading in 1s " "(claim number never filled, 0 CPT rows)"), out @case def hcfa_never_filled_returns_a_reason_not_an_exception(page): _go(page, HCFA, "load=never") out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000", timeout_ms=2_000) assert out == ("HCFA form did not finish loading in 2s " "(claim number never filled, 0 CPT rows)"), out assert 1.8 <= took < 5.0, f"took {took:.1f}s" assert out in log, log @case def hcfa_wrong_claim_raises(page): _go(page, HCFA, "load=wrong") try: _timed(claim_form.wait_claim_loaded, page, "9103000") except RuntimeError as exc: assert "5000001" in str(exc) and "wrong claim" in str(exc), exc else: raise AssertionError("a form showing another claim did not raise") @case def hcfa_without_a_claim_number_field_still_waits_on_cpt_rows(page): _go(page, HCFA, "load=noclaimno") out, took, log = _timed(claim_form.wait_claim_loaded, page, "9103000") assert out is None, out assert took >= 0.7, f"returned after {took:.2f}s — rows were still blank" assert log.count("identity not checked") == 1, log # ── Dental ──────────────────────────────────────────────────────────────────── @case def dental_filled_late_is_waited_for(page): _go(page, DENTAL, "load=delayed") out, took, log = _timed(claim_form.wait_claim_loaded, page, "5000000") assert out is None, out assert took >= 0.7, f"returned after {took:.2f}s — before the form filled" assert "Dental form loaded" in log and "2 service lines)" in log, log @case def dental_cancel_inside_a_dentalclaim_id_wrapper_still_reads_the_form(page): """ The window is found by walking up from the visible Cancel to the nearest SEL_DENTAL_WINDOW match. With an id-prefix-only selector, a `dentalClaimFooter…` wrapper around Cancel was that nearest match: no claim number and no service lines inside it, so the form read as blank and every Dental claim waited out the timeout as not evaluated. The window is the `.modal` itself now. """ _go(page, DENTAL, "load=delayed&footer=wrapped") assert page.locator("#dentalClaimFooter7777777777777 " "#cancelDentalClaimButton").count() == 1, "fixture did not wrap Cancel" out, took, log = _timed(claim_form.wait_claim_loaded, page, "5000000", timeout_ms=3_000) assert out is None, out assert took >= 0.7, f"returned after {took:.2f}s — before the form filled" assert "identity not checked" not in log, log assert "Dental form loaded" in log and "2 service lines)" in log, log @case def dental_never_filled_returns_a_reason(page): _go(page, DENTAL, "load=never") out, _, _ = _timed(claim_form.wait_claim_loaded, page, "5000000", timeout_ms=1_000) assert out == ("Dental form did not finish loading in 1s " "(claim number never filled, 0 service lines)"), out @case def dental_wrong_claim_raises(page): _go(page, DENTAL, "load=wrong") try: _timed(claim_form.wait_claim_loaded, page, "5000000") except RuntimeError as exc: assert "5000001" in str(exc), exc else: raise AssertionError("a Dental form showing another claim did not raise") @case def ub_form_is_left_to_open_claims_own_wait(page): """No-op on UB: open_claim() already waits for its claim number.""" _go(page, UB) out, took, _ = _timed(claim_form.wait_claim_loaded, page, "anything") assert out is None and took < 1.0, (out, took) # ── Elements that render late are waited for (final review, 2026-10-05) ────── # Both waits were `visible(timeout=10_000)` before P8 and answered instantly; # these fail if either is turned back into visible(). @case def a_cpt_grid_that_renders_late_is_waited_for(page): """open_cpt_hcpcs_tab gives the grid 10s after the tab click.""" _go(page, UB) page.evaluate("""() => { const g = document.querySelector('#CPTCodesDiv'); g.style.display = 'none'; setTimeout(() => { g.style.display = ''; }, 1800); }""") out, took, log = _timed(claim_form.open_cpt_hcpcs_tab, page) assert out is True, log assert took >= 1.5, f"returned after {took:.1f}s — before the grid showed" @case def a_prog_notes_button_that_renders_late_is_not_absent(page): """open_progress_notes gives the button 10s to appear.""" _go(page, HCFA) page.evaluate("""() => { const b = document.querySelector('[ng-click="viewProgNote()"]'); b.style.display = 'none'; setTimeout(() => { b.style.display = ''; }, 1500); }""") note, _, log = _timed(claim_form.open_progress_notes, page) assert note.get("opened") == "frame", (note, log) # ── Prog. Notes "disabled" is re-checked ────────────────────────────────────── @case def prog_notes_enabled_late_is_not_reported_disabled(page): """Angular enables the button only after load; read too early, a claim with a note was reported as having none.""" _go(page, HCFA, "pn=late") assert page.locator('[ng-click="viewProgNote()"]').is_disabled() note, _, _ = _timed(claim_form.open_progress_notes, page) assert note.get("opened") == "frame", note @case def prog_notes_disabled_for_good_is_reported_after_about_5s(page): _go(page, HCFA, "pn=off") note, took, log = _timed(claim_form.open_progress_notes, page) assert note == {"opened": "disabled"}, note assert 4.5 <= took < 9.0, f"took {took:.1f}s" # ── The flow keeps the reason on the claim's data ───────────────────────────── def _flow_read(dental: bool, not_loaded, dental_result, apply=False, calls=None, notes=False, dental_checks_on=True, results=(), save=False): """ Run the real _read_claim_form with every page step stubbed. `calls`, when given, collects ("run_all" | "save_claim" | "dental" | "wait" | "notes", ...) as they happen. `notes` turns OPEN_PROGRESS_NOTES on; `dental_checks_on` is CHECK_HIGH_RISK (CHECK_SLIDING_FEE stays off). """ calls = [] if calls is None else calls def wait(page, cid): calls.append("wait") return not_loaded def run_all(page, cid, data, before_apply=None, **kw): calls.append("run_all") return list(results) def dental_checks(page, cid): calls.append("dental") return dict(dental_result) stubs = { "open_claim": lambda page, cid: True, "wait_claim_loaded": wait, "open_progress_notes": lambda page: calls.append("notes") or { "opened": "frame", "sections": {"Procedure Codes": "x"}}, "is_dental_claim_form": lambda page: dental, "open_cpt_hcpcs_tab": lambda page: None, "extract": lambda page: {"cpt": [{"code": "99213"}], "icd": [], "revenue": []}, "save": lambda data: pathlib.Path("unused.json"), "close_claim": lambda page: True, "save_claim": lambda page: calls.append("save_claim") or True, } orig_cf = {k: getattr(claim_form, k) for k in stubs} orig_cfg = (cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, cfg.APPLY_CORRECTIONS, cfg.CHECK_HIGH_RISK, cfg.CHECK_SLIDING_FEE, cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE) orig_box = (flow.msgbox.ok_cancel, flow.msgbox.info_box) orig_dental, orig_run_all = flow._dental_checks, flow.conditions.run_all for k, v in stubs.items(): setattr(claim_form, k, v) cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES = True, notes cfg.APPLY_CORRECTIONS = apply # Not read from the operator's .env: a Dental case needs a check on to # reach _dental_checks at all (final review, 2026-10-05). cfg.CHECK_HIGH_RISK, cfg.CHECK_SLIDING_FEE = dental_checks_on, False cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE = save, False # Save, if ever offered, is answered OK — so "not saved" is the code's # doing, not the operator's. flow.msgbox.ok_cancel = lambda *a, **k: calls.append("save_box") or True flow.msgbox.info_box = lambda *a, **k: None flow._dental_checks = dental_checks flow.conditions.run_all = run_all try: with contextlib.redirect_stdout(io.StringIO()): return flow._read_claim_form(None, "9103000") finally: for k, v in orig_cf.items(): setattr(claim_form, k, v) (cfg.OPEN_CLAIM_FORM, cfg.OPEN_PROGRESS_NOTES, cfg.APPLY_CORRECTIONS, cfg.CHECK_HIGH_RISK, cfg.CHECK_SLIDING_FEE, cfg.SAVE_CORRECTIONS, cfg.REVIEW_BEFORE_CLOSE) = orig_cfg flow.msgbox.ok_cancel, flow.msgbox.info_box = orig_box flow._dental_checks = orig_dental flow.conditions.run_all = orig_run_all @case def flow_marks_an_hcfa_claim_not_evaluated_and_still_reads_it(): why = "HCFA form did not finish loading in 20s (0 CPT rows)" data = _flow_read(False, why, {}) assert data.get("_not_evaluated") == why, data assert data.get("cpt"), "the claim was not read after the timeout" @case def flow_leaves_a_loaded_claim_unmarked(): data = _flow_read(False, None, {}) assert "_not_evaluated" not in data, data @case def flow_marks_the_dental_result_not_evaluated(): why = "Dental form did not finish loading in 20s (0 service lines)" data = _flow_read(True, why, {"claim_no": "9103000", "high_risk": {"high_risk": False}}) assert data.get("_not_evaluated") == why, data @case def flow_runs_no_conditions_on_a_form_that_did_not_finish_loading(): """ APPLY_CORRECTIONS=1: a half-loaded form is read and closed, never corrected — run_all() would read the same blank rows, and with SAVE_CORRECTIONS=1 an approved OK would save the half-loaded form back to live eCW. Nothing ran, so no `_conditions` either. """ calls = [] why = "HCFA form did not finish loading in 20s (0 CPT rows)" data = _flow_read(False, why, {}, apply=True, calls=calls) assert "run_all" not in calls and "save_claim" not in calls, calls assert data.get("_not_evaluated") == why, data assert "_conditions" not in data, data @case def flow_still_runs_conditions_on_a_loaded_form(): """The control for the case above: the run_all stub is reachable.""" calls = [] data = _flow_read(False, None, {}, apply=True, calls=calls) # "wait": _flow_read records the load wait too since the final review. assert calls == ["wait", "run_all"], calls # Unit D (2026-10-05): run_all's report rides along too, so the summary # can tell "raised / declined" from "clean". # `changes` (2026-10-08): what each condition changed, for the results DB. # `detected` / `changed` (2026-10-12): the same, split for the DB. assert data.get("_conditions") == {"fired": [], "detected": [], "changed": [], "saved": False, "failed": [], "review": [], "comments": {}, "changes": {}}, data @case def flow_tells_the_dental_box_the_form_did_not_finish_loading(): """ Updated for review item D9 (2026-10-05): `_dental_checks` no longer shows a box, so the reason is no longer passed into it. It reaches the claim's one box (`_claim_box`, after close) on the data instead, and that box's Dental part leads with NOT_LOADED_NOTE. """ calls = [] why = "Dental form did not finish loading in 20s (0 service lines)" data = _flow_read(True, why, {"claim_no": "9103000", "high_risk": {"high_risk": False, "match": ""}}, calls=calls) assert calls == ["wait", "dental"], calls assert data["_not_evaluated"] == why, data _, body = flow._claim_box("Claim 1/1", "9103000", data, None) detail = body.split(flow.RULE + "\n", 1)[1].splitlines() assert detail[0] == claim_form.NOT_LOADED_NOTE, body assert detail[1] == f"({why})", body @case def claim_summary_leads_with_the_not_loaded_line(): """The SHOW_EACH_CLAIM box must not show a confident "CPT 0" read off a form the bot logged as half-loaded.""" why = "HCFA form did not finish loading in 20s (0 CPT rows)" text = claim_form.summary({"claim_no": "9103000", "cpt": [], "_not_evaluated": why}) first = text.splitlines() assert first[0] == claim_form.NOT_LOADED_NOTE, text assert first[1] == f"({why})", text plain = claim_form.summary({"claim_no": "9103000", "cpt": []}) assert claim_form.NOT_LOADED_NOTE not in plain, plain assert plain.splitlines()[0].startswith("Claim No"), plain @case def flow_reads_a_dental_claim_even_with_no_dental_check_on(): """ Operator, 2026-10-12: a Dental claim runs every applicable condition, not only high risk / sliding fee — so it is waited for and read like any claim, whatever CHECK_HIGH_RISK / CHECK_SLIDING_FEE say. (It used to come back {} and blank in the summary.) """ calls = [] data = _flow_read(True, None, {}, calls=calls, dental_checks_on=False, apply=True) assert calls == ["wait", "run_all"], calls assert data.get("screen") == "dental", data assert data.get("cpt"), data assert data.get("_conditions") is not None, data @case def flow_runs_the_conditions_on_a_dental_claim_after_its_own_checks(): calls = [] data = _flow_read(True, None, {"claim_no": "9103000", "high_risk": {"high_risk": True, "match": "x"}}, calls=calls, apply=True, notes=True) assert calls == ["wait", "dental", "notes", "run_all"], calls assert data["high_risk"]["high_risk"] is True, "Dental result kept" assert data.get("screen") == "dental" and data.get("cpt"), data @case def flow_never_saves_a_dental_claim_even_with_save_on(): """Operator, 2026-10-12: Dental row writes are not live-verified yet.""" from ecw_bot.conditions.base import ConditionResult fired = ConditionResult(condition_name="dummy_codes", changed=True, changes=[{"row": 1, "code": "DCONS", "unchecked": True}]) calls = [] data = _flow_read(True, None, {}, calls=calls, apply=True, save=True, results=[fired], dental_checks_on=False) assert "save_box" not in calls and "save_claim" not in calls, calls assert data["_conditions"]["saved"] is False, data # The gate itself, and its control: the same result on a read that is # not Dental IS offered for Save. asked = [] orig = (cfg.SAVE_CORRECTIONS, flow.msgbox.ok_cancel) cfg.SAVE_CORRECTIONS = True flow.msgbox.ok_cancel = lambda *a, **k: asked.append(1) or False try: with contextlib.redirect_stdout(io.StringIO()): flow._confirm_and_save(None, "1", [fired], {}, {"screen": "dental"}) assert asked == [], "a Dental claim was offered for Save" flow._confirm_and_save(None, "1", [fired], {}, None) finally: cfg.SAVE_CORRECTIONS, flow.msgbox.ok_cancel = orig assert asked == [1], "the control was not offered" @case def flow_still_waits_on_a_dental_form_when_a_check_is_on(): calls = [] _flow_read(True, None, {"claim_no": "9103000", "high_risk": {"high_risk": False}}, calls=calls) assert calls == ["wait", "dental"], calls @case def flow_does_not_open_the_note_of_a_form_that_did_not_finish_loading(): """ Final review, 2026-10-05: the conditions are skipped on such a claim and it is Review whatever the note says, so the note's up to ~45s (absent 10s, disabled 5s, nothing 20s + close) is not spent. The note is recorded as skipped, and the summary says it was not read. """ from ecw_bot.core import charge_summary as cs calls = [] why = "HCFA form did not finish loading in 20s (0 CPT rows)" data = _flow_read(False, why, {}, apply=True, calls=calls, notes=True) assert "notes" not in calls, calls assert data["progress_notes"]["opened"] == "skipped", data reasons = cs.review_reasons(data) assert reasons[0] == why, reasons assert "Progress note not read (skipped) — telehealth not checked" in \ reasons, reasons @case def flow_opens_the_note_of_a_loaded_form(): """The control: the stub is reachable and the note is read.""" calls = [] data = _flow_read(False, None, {}, calls=calls, notes=True) assert "notes" in calls, calls assert data["progress_notes"]["opened"] == "frame", data # ── The claim number is compared as a number (final review, 2026-10-05) ────── @case def same_claim_no_ignores_formatting_but_not_a_different_number(): same = claim_form.same_claim_no assert same("5000000", "5000000") assert same("05000000", "5000000") assert same("\u00a05000\u00a0000 ", "5000000") assert same("Claim # 5000000", "5000000") assert not same("5000001", "5000000") assert not same("50000001", "5000000") assert not same("", "5000000") assert same("abc", "abc") and not same("abc", "abd") @case def dental_claim_number_written_differently_is_not_a_wrong_claim(page): """ The Dental claim-number span is not yet live-verified: a leading zero or an inner non-breaking space must not raise "wrong claim" on every Dental claim. It is logged once, so the live format is on record. """ _go(page, DENTAL) page.evaluate("""() => { document.querySelector('[ng-bind="ClaimData.claimNo"]') .textContent = '0500\u00a00000'; }""") out, _, log = _timed(claim_form.wait_claim_loaded, page, "5000000", timeout_ms=3_000) assert out is None, (out, log) assert log.count("the same claim, written differently") == 1, log @case def ub_claim_number_that_never_filled_is_a_not_loaded_reason(page): """ Final review, 2026-10-05: open_claim() only logged "Claim No never filled in — reading anyway", so the conditions ran on a UB form that may be half-loaded. The reason now reaches the claim's data, and the flow's not-evaluated branch skips the conditions as for HCFA/Dental. """ _go(page, UB) page.evaluate("""() => { for (const e of document.querySelectorAll( '#UBClaimDiv .ubClaimNumberText span')) e.textContent = ''; }""") out, took, _ = _timed(claim_form.wait_claim_loaded, page, "9104000") assert out == ("UB form did not finish loading (claim number never " "filled)"), out assert took < 1.0, f"took {took:.1f}s — open_claim already waited" @case def flow_reports_a_wrong_claim_as_unread_and_still_closes_it(): closed = [] def wrong(page, cid): raise RuntimeError("Opened the form for claim '5000001'") orig = (claim_form.open_claim, claim_form.wait_claim_loaded, claim_form.close_claim, cfg.OPEN_CLAIM_FORM) claim_form.open_claim = lambda page, cid: True claim_form.wait_claim_loaded = wrong claim_form.close_claim = lambda page: closed.append(True) cfg.OPEN_CLAIM_FORM = True try: with contextlib.redirect_stdout(io.StringIO()): data = flow._read_claim_form(None, "9103000") finally: (claim_form.open_claim, claim_form.wait_claim_loaded, claim_form.close_claim, cfg.OPEN_CLAIM_FORM) = orig # Unit D (2026-10-05): not {} any more — {} read exactly like "not # found". The error rides on the data and the summary says Review. assert data == {"claim_no": "9103000", "_error": "Opened the form for claim '5000001'"}, data assert closed == [True], "the wrong claim's form was left open" # ── close_claim() on a page error ──────────────────────────────────────────── @case def any_claim_form_open_raises_on_a_page_error_rather_than_reading_closed(): """ visible() swallows errors and answers False; close_claim() reads False as "already closed — nothing to cancel". any_claim_form_open() therefore keeps its own raising check (review of unit C, 2026-10-05). """ class Broken: def locator(self, selector): raise RuntimeError("Target page, context or browser has been closed") try: claim_form.any_claim_form_open(Broken()) except RuntimeError: pass else: raise AssertionError("a page error read as an answer, not an error") @case def close_claim_still_clicks_cancel_when_the_open_check_errors(page): """A transient error while asking is not "closed": Cancel is still clicked (the form would otherwise stay open for the next claim), and the run is not ended by a raise from the flow's `finally`.""" _go(page, DENTAL) orig = claim_form.any_claim_form_open def broken(page): raise RuntimeError("Execution context was destroyed") claim_form.any_claim_form_open = broken try: out, _, log = _timed(claim_form.close_claim, page) finally: claim_form.any_claim_form_open = orig assert out is True, (out, log) assert "trying Cancel anyway" in log, log assert "already closed" not in log, log assert not page.locator("#cancelDentalClaimButton").is_visible(), "Cancel was not clicked — the form is still open" def main() -> int: try: from playwright.sync_api import sync_playwright except ImportError: print(" SKIP playwright is not installed") return 0 failures = 0 orig_shots = cfg.SCREENSHOTS with tempfile.TemporaryDirectory() as tmp, sync_playwright() as pw: cfg.SCREENSHOTS = pathlib.Path(tmp) browser = pw.chromium.launch(headless=True) page = browser.new_page() try: for fn in CASES: name = fn.__name__.replace("_", " ") try: if len(inspect.signature(fn).parameters) == 0: fn() else: fn(page) 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 finally: cfg.SCREENSHOTS = orig_shots browser.close() print(f"\n{len(CASES) - failures}/{len(CASES)} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())