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)