Spaces:
Running
Running
File size: 6,902 Bytes
df692ec 7507549 df692ec efee09f 7507549 df692ec | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | """Local E2E of the homeowner widget. The model pipeline is mocked at the network layer
(Playwright route interception) — no server fake needed, no models, no Google $."""
from __future__ import annotations
import base64
import json
import pytest
from playwright.sync_api import expect
pytestmark = pytest.mark.local
# 1x1 transparent PNG — enough for the overlay <img>; the editable SVG polygon overlays it.
_PNG = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC")
_QUOTE = {
"result_id": "e2e-r1", "formatted_address": "5534 Mayberry St, Omaha, NE 68106",
"lawn_sqft": 4200, "confidence": "high", "method": "lidar+rgb", "has_overlay": True,
"lawn_polygon_px": [[300, 320], [980, 300], [1010, 980], [280, 1000]],
"pixel_area_sqft": 0.35, "image_size_px": [1280, 1280],
"pricing": {"currency": "USD", "line_items": [
{"service": "mow", "label": "Mowing", "unit": "per_1000_sqft", "rate": 10, "rate_per_1000_sqft": 10, "min_charge": 30},
{"service": "fert", "label": "Fertilizer", "unit": "per_1000_sqft", "rate": 8, "rate_per_1000_sqft": 8, "min_charge": 25}],
"bundles": [{"id": "spring", "name": "Spring Package", "cadence": "seasonal", "mode": "fixed",
"price": 120.0, "subtotal": 140.0, "savings": 20.0, "line_items": [
{"service": "mow", "label": "Mowing", "unit": "per_1000_sqft", "rate": 10, "rate_per_1000_sqft": 10, "min_charge": 30, "price": 42.0},
{"service": "fert", "label": "Fertilizer", "unit": "per_1000_sqft", "rate": 8, "rate_per_1000_sqft": 8, "min_charge": 25, "price": 33.6}]}]},
}
def test_widget_quote_outline_and_book(page, app_url):
booked = {"n": 0}
def _json(route, obj):
route.fulfill(status=200, content_type="application/json", body=json.dumps(obj))
page.route("**/autocomplete*", lambda r: _json(r, []))
page.route("**/quote", lambda r: _json(r, _QUOTE))
page.route("**/overlay/*.png", lambda r: r.fulfill(status=200, content_type="image/png", body=_PNG))
page.route("**/lead", lambda r: _json(r, {}))
def _book(route):
booked["n"] += 1
_json(route, {"booked": True, "notified": True})
page.route("**/book", _book)
page.goto(f"{app_url}/widget?key=pub-dev", wait_until="networkidle")
page.wait_for_selector("#address", timeout=15000)
page.fill("#address", "5534 Mayberry St, Omaha, NE 68106")
page.click("#go")
# lead-capture popup covers the (mocked) measurement
page.wait_for_selector("#overlay.show", timeout=8000)
page.fill("#email", "sam@example.com")
page.click("#leadGo")
# price + editable outline revealed
page.wait_for_selector("#pricebox", timeout=10000)
page.wait_for_selector("#lawnpic svg.lawnsvg", timeout=8000)
assert page.locator("#lawnpic svg.lawnsvg .vh").count() >= 4 # draggable outline handles
assert page.locator("#checkout input[data-svc]").count() >= 1 # services (default-selected)
# book: T&C + submit (services are pre-checked)
page.check("#tnc")
page.click("#bookGo")
expect(page.locator("#checkout")).to_contain_text("reach out", timeout=8000)
assert booked["n"] == 1
def test_widget_editor_add_point_on_a_side(page, app_url):
# Parity with the dashboard editor: every side has a ghost handle; dragging one inserts a vertex.
def _json(route, obj):
route.fulfill(status=200, content_type="application/json", body=json.dumps(obj))
page.route("**/autocomplete*", lambda r: _json(r, []))
page.route("**/quote", lambda r: _json(r, _QUOTE))
page.route("**/overlay/*.png", lambda r: r.fulfill(status=200, content_type="image/png", body=_PNG))
page.route("**/lead", lambda r: _json(r, {}))
page.goto(f"{app_url}/widget?key=pub-dev", wait_until="networkidle")
page.wait_for_selector("#address", timeout=15000)
page.fill("#address", "5534 Mayberry St, Omaha, NE 68106")
page.click("#go")
page.wait_for_selector("#overlay.show", timeout=8000)
page.fill("#email", "sam@example.com")
page.click("#leadGo")
page.wait_for_selector("#lawnpic svg.lawnsvg .mh", timeout=10000)
assert page.locator("#lawnpic svg.lawnsvg .mh").count() >= 4 # a ghost on every side
v0 = page.locator("#lawnpic svg.lawnsvg .vh").count()
mh = page.locator("#lawnpic svg.lawnsvg .mh").first
box = mh.bounding_box()
page.mouse.move(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2)
page.mouse.down()
page.mouse.move(box["x"] + 30, box["y"] + 30, steps=6)
page.mouse.up()
page.wait_for_timeout(200)
assert page.locator("#lawnpic svg.lawnsvg .vh").count() == v0 + 1 # a point was inserted
def test_widget_book_a_package(page, app_url):
captured = {}
def _json(route, obj):
route.fulfill(status=200, content_type="application/json", body=json.dumps(obj))
page.route("**/autocomplete*", lambda r: _json(r, []))
page.route("**/quote", lambda r: _json(r, _QUOTE))
page.route("**/overlay/*.png", lambda r: r.fulfill(status=200, content_type="image/png", body=_PNG))
page.route("**/lead", lambda r: _json(r, {}))
def _book(route):
captured.update(route.request.post_data_json)
_json(route, {"booked": True, "notified": True})
page.route("**/book", _book)
page.goto(f"{app_url}/widget?key=pub-dev", wait_until="networkidle")
page.wait_for_selector("#address", timeout=15000)
page.fill("#address", "5534 Mayberry St, Omaha, NE 68106")
page.click("#go")
page.wait_for_selector("#overlay.show", timeout=8000)
page.fill("#email", "sam@example.com")
page.click("#leadGo")
# A package option is offered; choosing it sets the total to the bundle price.
page.wait_for_selector("#checkout .pkg", timeout=10000)
page.check('#checkout .pkg input[value="spring"]')
expect(page.locator("#bookTotal")).to_contain_text("120")
page.check("#tnc")
page.click("#bookGo")
expect(page.locator("#checkout")).to_contain_text("reach out", timeout=8000)
assert captured.get("bundle_id") == "spring" and captured.get("cadence") == "seasonal"
def test_widget_bad_address_shows_message(page, app_url):
# a 422 from /quote → a friendly on-page message, not a crash
page.route("**/quote", lambda r: r.fulfill(
status=422, content_type="application/json",
body=json.dumps({"detail": "We couldn't find that address."})))
page.goto(f"{app_url}/widget?key=pub-dev", wait_until="networkidle")
page.wait_for_selector("#address", timeout=15000)
page.fill("#address", "not a real place")
page.click("#go")
page.wait_for_selector("#overlay.show", timeout=8000)
page.fill("#email", "x@y.com")
page.click("#leadGo")
expect(page.locator("#state")).to_contain_text("address", timeout=8000)
|