mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-10-07 15:08:55 +00:00
fix(cart): report cart failures without a traceback
A closed window or a failed cart request during the fill now prints a short failure and the Added/Failed/Skipped summary instead of crashing.
This commit is contained in:
parent
dce33918b7
commit
ca04a117f5
2 changed files with 136 additions and 59 deletions
|
|
@ -104,65 +104,7 @@ def _fill(plan: FillPlan, menu_url: str) -> int:
|
||||||
return 1
|
return 1
|
||||||
page = browser.new_page()
|
page = browser.new_page()
|
||||||
try:
|
try:
|
||||||
page.goto(menu_url, wait_until="domcontentloaded", timeout=60000)
|
return _fill_page(page, plan, menu_url)
|
||||||
cart = _cart_request(page, "GET", "/api/cart")
|
|
||||||
existing = cart_lines(cart)
|
|
||||||
if existing:
|
|
||||||
print("The cart already has items:")
|
|
||||||
for line in existing:
|
|
||||||
print(f" {line.name} x{line.quantity}")
|
|
||||||
if not _confirm_clear():
|
|
||||||
print("Exiting without adding.")
|
|
||||||
return 1
|
|
||||||
cleared = _cart_request(page, "POST", "/api/cart/clear", {})
|
|
||||||
if cart_lines(cleared):
|
|
||||||
print(
|
|
||||||
"The cart still has items after clear. Exiting without adding."
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
failed: list[str] = []
|
|
||||||
posted = []
|
|
||||||
for item in plan.adds:
|
|
||||||
try:
|
|
||||||
_cart_request(
|
|
||||||
page,
|
|
||||||
"POST",
|
|
||||||
"/api/cart/add",
|
|
||||||
{
|
|
||||||
"uuid": item.uuid,
|
|
||||||
"quantity": item.quantity,
|
|
||||||
"properties": None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except RuntimeError as exc:
|
|
||||||
failed.append(f"{item.name}: {exc}")
|
|
||||||
print(f"Failed: {item.name}: {exc}")
|
|
||||||
continue
|
|
||||||
posted.append(item)
|
|
||||||
|
|
||||||
added = 0
|
|
||||||
try:
|
|
||||||
current = cart_lines(_cart_request(page, "GET", "/api/cart"))
|
|
||||||
except RuntimeError as exc:
|
|
||||||
failed.append(f"Could not read the cart: {exc}")
|
|
||||||
print(f"Failed: Could not read the cart: {exc}")
|
|
||||||
else:
|
|
||||||
for item in posted:
|
|
||||||
found = quantity_for(current, item.uuid)
|
|
||||||
if found != item.quantity:
|
|
||||||
message = (
|
|
||||||
f"{item.name}: requested {item.quantity}, cart has {found}"
|
|
||||||
)
|
|
||||||
failed.append(message)
|
|
||||||
print(f"Failed: {message}")
|
|
||||||
else:
|
|
||||||
added += 1
|
|
||||||
origin = _origin(page.url or menu_url)
|
|
||||||
page.goto(f"{origin}/cart", wait_until="domcontentloaded", timeout=60000)
|
|
||||||
print(f"Added {added}. Failed {len(failed)}. Skipped {len(plan.skipped)}.")
|
|
||||||
_wait_for_review(page)
|
|
||||||
return 1 if failed or plan.skipped else 0
|
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
@ -170,6 +112,73 @@ def _fill(plan: FillPlan, menu_url: str) -> int:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_page(page, plan: FillPlan, menu_url: str) -> int:
|
||||||
|
failed: list[str] = []
|
||||||
|
added = 0
|
||||||
|
ready = False
|
||||||
|
try:
|
||||||
|
page.goto(menu_url, wait_until="domcontentloaded", timeout=60000)
|
||||||
|
cart = _cart_request(page, "GET", "/api/cart")
|
||||||
|
existing = cart_lines(cart)
|
||||||
|
if existing:
|
||||||
|
print("The cart already has items:")
|
||||||
|
for line in existing:
|
||||||
|
print(f" {line.name} x{line.quantity}")
|
||||||
|
if not _confirm_clear():
|
||||||
|
print("Exiting without adding.")
|
||||||
|
return 1
|
||||||
|
cleared = _cart_request(page, "POST", "/api/cart/clear", {})
|
||||||
|
if cart_lines(cleared):
|
||||||
|
print("The cart still has items after clear. Exiting without adding.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
posted = []
|
||||||
|
for item in plan.adds:
|
||||||
|
try:
|
||||||
|
_cart_request(
|
||||||
|
page,
|
||||||
|
"POST",
|
||||||
|
"/api/cart/add",
|
||||||
|
{
|
||||||
|
"uuid": item.uuid,
|
||||||
|
"quantity": item.quantity,
|
||||||
|
"properties": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
failed.append(f"{item.name}: {exc}")
|
||||||
|
print(f"Failed: {item.name}: {exc}")
|
||||||
|
continue
|
||||||
|
posted.append(item)
|
||||||
|
|
||||||
|
try:
|
||||||
|
current = cart_lines(_cart_request(page, "GET", "/api/cart"))
|
||||||
|
except RuntimeError as exc:
|
||||||
|
failed.append(f"Could not read the cart: {exc}")
|
||||||
|
print(f"Failed: Could not read the cart: {exc}")
|
||||||
|
else:
|
||||||
|
for item in posted:
|
||||||
|
found = quantity_for(current, item.uuid)
|
||||||
|
if found != item.quantity:
|
||||||
|
message = (
|
||||||
|
f"{item.name}: requested {item.quantity}, cart has {found}"
|
||||||
|
)
|
||||||
|
failed.append(message)
|
||||||
|
print(f"Failed: {message}")
|
||||||
|
else:
|
||||||
|
added += 1
|
||||||
|
origin = _origin(page.url or menu_url)
|
||||||
|
page.goto(f"{origin}/cart", wait_until="domcontentloaded", timeout=60000)
|
||||||
|
ready = True
|
||||||
|
except (RuntimeError, PlaywrightError) as exc:
|
||||||
|
failed.append(str(exc))
|
||||||
|
print(f"Failed: {exc}")
|
||||||
|
print(f"Added {added}. Failed {len(failed)}. Skipped {len(plan.skipped)}.")
|
||||||
|
if ready:
|
||||||
|
_wait_for_review(page)
|
||||||
|
return 1 if failed or plan.skipped else 0
|
||||||
|
|
||||||
|
|
||||||
def _cart_request(page, method: str, path: str, body: object | None = None) -> object:
|
def _cart_request(page, method: str, path: str, body: object | None = None) -> object:
|
||||||
result = page.evaluate(_CART_JS, {"method": method, "path": path, "body": body})
|
result = page.evaluate(_CART_JS, {"method": method, "path": path, "body": body})
|
||||||
if not isinstance(result, dict) or not result.get("ok"):
|
if not isinstance(result, dict) or not result.get("ok"):
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
"""CSV parsing and menu matching for the Redefine cart filler."""
|
"""CSV parsing and menu matching for the Redefine cart filler."""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from playwright.sync_api import Error as PlaywrightError
|
||||||
|
|
||||||
from scraper.fill_cart import build_plan, cart_lines, error_text, quantity_for
|
from scraper.fill_cart import build_plan, cart_lines, error_text, quantity_for
|
||||||
from scraper.parse_menu import extract_catalog_products
|
from scraper.parse_menu import extract_catalog_products
|
||||||
|
|
||||||
|
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "fill_redefine_cart.py"
|
||||||
|
_spec = importlib.util.spec_from_file_location("fill_redefine_cart", _SCRIPT)
|
||||||
|
fill_redefine_cart = importlib.util.module_from_spec(_spec)
|
||||||
|
assert _spec.loader is not None
|
||||||
|
_spec.loader.exec_module(fill_redefine_cart)
|
||||||
|
|
||||||
PRODUCTS = [
|
PRODUCTS = [
|
||||||
{
|
{
|
||||||
"name": "Korean Steak Bowl",
|
"name": "Korean Steak Bowl",
|
||||||
|
|
@ -123,6 +134,63 @@ def test_cart_lines_use_the_product_uuid_and_quantity():
|
||||||
assert quantity_for(lines, "line-1") == 0
|
assert quantity_for(lines, "line-1") == 0
|
||||||
|
|
||||||
|
|
||||||
|
class _CartPage:
|
||||||
|
def __init__(self, responses):
|
||||||
|
self._responses = list(responses)
|
||||||
|
|
||||||
|
def goto(self, url, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def evaluate(self, script, payload):
|
||||||
|
response = self._responses.pop(0)
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
raise response
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def test_fill_page_prints_a_summary_when_the_window_closes_mid_add(capsys):
|
||||||
|
plan = build_plan("Item,Quantity\nKorean Steak Bowl,1\n", PRODUCTS)
|
||||||
|
page = _CartPage(
|
||||||
|
[
|
||||||
|
{"ok": True, "status": 200, "data": {"items": []}},
|
||||||
|
PlaywrightError("Target page, context or browser has been closed"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
code = fill_redefine_cart._fill_page(
|
||||||
|
page, plan, "https://www.redefinemeals.com/menu"
|
||||||
|
)
|
||||||
|
output = capsys.readouterr().out
|
||||||
|
|
||||||
|
assert code == 1
|
||||||
|
assert "Traceback" not in output
|
||||||
|
assert "Added 0. Failed 1. Skipped 0." in output
|
||||||
|
assert "Cart is ready" not in output
|
||||||
|
|
||||||
|
|
||||||
|
def test_fill_page_prints_a_summary_when_the_first_cart_read_fails(capsys):
|
||||||
|
plan = build_plan("Item,Quantity\nKorean Steak Bowl,1\n", PRODUCTS)
|
||||||
|
page = _CartPage(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"status": 500,
|
||||||
|
"data": {"message": "cart unavailable", "trace": [{"file": "x"}]},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
code = fill_redefine_cart._fill_page(
|
||||||
|
page, plan, "https://www.redefinemeals.com/menu"
|
||||||
|
)
|
||||||
|
output = capsys.readouterr().out
|
||||||
|
|
||||||
|
assert code == 1
|
||||||
|
assert "HTTP 500: cart unavailable" in output
|
||||||
|
assert "x" not in output.split("Failed:", 1)[-1]
|
||||||
|
assert "Added 0. Failed 1. Skipped 0." in output
|
||||||
|
|
||||||
|
|
||||||
def test_error_text_keeps_the_message_and_drops_the_trace():
|
def test_error_text_keeps_the_message_and_drops_the_trace():
|
||||||
body = {
|
body = {
|
||||||
"message": "Product is not available.",
|
"message": "Product is not available.",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue