- Batteries: one card per battery with On/Off, the mode list (cheap hours and selling greyed out until OPT-11 P3), level, and Now / Today / Why lines built in homeview.py. Replaces the OPT-11 P1 minimal cards and uses the existing pack_use / pack_mode actions and store keys. - An Off battery the peak limit still needs says so with its figure; no line shows 0 W beside moving power (OPT-11 P1 test F2). Today says "Not planned yet" until OPT-11 P3/P4. - Home's plan leaves Off batteries out, except for the peak limit. - Settings opens on Simple: the layout's simple fields with an applies chip and a note, plus the battery switches and modes. Installer is one tap. - English and Dutch words in app/ui_text; test_batteries_screen.py; browser check tools/ux13_browser_check.py (CHECK=1 ux13_screenshots.sh); new and changed screenshots only. No control, allocator, arbiter or command module changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV
185 lines
7.3 KiB
Python
185 lines
7.3 KiB
Python
"""UX-13 P3: the headless-browser check of the Batteries screen and Settings,
|
|
Simple. Run by `CHECK=1 tools/ux13_screenshots.sh`, like the screenshots.
|
|
|
|
For every battery-mode fixture (tools/ux13_demo.py) it opens both screens at
|
|
phone and desktop width, light and dark, in English and Dutch, and fails on a
|
|
page error, a console error or a sideways scroll. Then it clicks the switch
|
|
and the mode list on both screens and checks what was posted and what the
|
|
cards show after.
|
|
|
|
python3 ux13_browser_check.py # DEMO_<STATE> = the fixture's URL
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
STATES = ("running", "modes", "off", "off_peak", "refusal", "maint")
|
|
SIZES = {"phone": {"width": 390, "height": 844},
|
|
"desktop": {"width": 1000, "height": 900}}
|
|
FAILS = []
|
|
|
|
|
|
def url(state):
|
|
return os.environ[f"DEMO_{state.upper()}"]
|
|
|
|
|
|
def check(name, cond):
|
|
print(f" {'ok ' if cond else 'FAIL'} {name}")
|
|
if not cond:
|
|
FAILS.append(name)
|
|
|
|
|
|
def open_page(browser, state, route, *, size, scheme, lang):
|
|
ctx = browser.new_context(locale=lang, viewport=SIZES[size],
|
|
color_scheme=scheme, reduced_motion="reduce")
|
|
page = ctx.new_page()
|
|
errors = []
|
|
page.on("pageerror", lambda e: errors.append(f"pageerror {e}"))
|
|
page.on("console", lambda m: errors.append(f"console {m.text}")
|
|
if m.type == "error" else None)
|
|
page.goto(url(state) + route)
|
|
page.wait_for_load_state("networkidle")
|
|
page.wait_for_timeout(1200)
|
|
return ctx, page, errors
|
|
|
|
|
|
def sweep(browser):
|
|
n = 0
|
|
for state in STATES:
|
|
for route in ("#batteries", "#settings"):
|
|
for size in SIZES:
|
|
for scheme in ("light", "dark"):
|
|
for lang in ("en-GB", "nl-BE"):
|
|
ctx, page, errors = open_page(
|
|
browser, state, route, size=size, scheme=scheme,
|
|
lang=lang)
|
|
wide = page.evaluate(
|
|
"document.documentElement.scrollWidth > "
|
|
"document.documentElement.clientWidth")
|
|
if route == "#batteries":
|
|
cards = page.locator("#bcards .bcard").count()
|
|
texts = page.locator("#bcards dd").all_inner_texts()
|
|
ok = cards == 2 and all(t.strip() for t in texts)
|
|
else:
|
|
ok = (page.locator("#simple .card").count() >= 3
|
|
and page.locator("#groups").is_hidden())
|
|
where = f"{state} {route} {size} {scheme} {lang}"
|
|
if errors or wide or not ok:
|
|
FAILS.append(f"{where}: errors={errors} "
|
|
f"wide={wide} drawn={ok}")
|
|
print(f" FAIL {where}: {errors} wide={wide} "
|
|
f"drawn={ok}")
|
|
n += 1
|
|
ctx.close()
|
|
check(f"{n} views: 0 page errors, 0 console errors, no sideways scroll, "
|
|
f"every card drawn", not [f for f in FAILS if " #" in f])
|
|
|
|
|
|
def posts(page):
|
|
sent = []
|
|
page.on("request", lambda r: sent.append(json.loads(r.post_data))
|
|
if r.url.endswith("/action") and r.method == "POST" else None)
|
|
return sent
|
|
|
|
|
|
def actions(browser):
|
|
# The Batteries screen: switch B off, then set A to Leftover solar.
|
|
ctx, page, errors = open_page(browser, "modes", "#batteries",
|
|
size="desktop", scheme="light", lang="en-GB")
|
|
sent = posts(page)
|
|
page.locator("#bsw-c1").click()
|
|
page.wait_for_timeout(1500)
|
|
card_b = page.locator("#bcards .bcard").nth(1)
|
|
check("Batteries: B's switch posts pack_use false, and B's card turns Off "
|
|
"with its mode list greyed out",
|
|
sent[-1:] == [{"action": "pack_use", "unit": "Battery B",
|
|
"value": False}]
|
|
and "off" in (card_b.get_attribute("class") or "")
|
|
and page.locator("#bsel-c1").is_disabled()
|
|
and "You switched it off." in card_b.inner_text())
|
|
page.locator("#bsel-c0").focus()
|
|
page.locator("#bsel-c0").select_option("leftover_solar")
|
|
page.wait_for_timeout(1500)
|
|
check("...A's list posts pack_mode leftover_solar and the card keeps it",
|
|
sent[-1:] == [{"action": "pack_mode", "unit": "Battery A",
|
|
"value": "leftover_solar"}]
|
|
and page.locator("#bsel-c0").input_value() == "leftover_solar"
|
|
and "Leftover solar" in page.locator("#bcards .bcard").nth(0)
|
|
.inner_text())
|
|
check("...the cheap-hours choice and the selling switch are greyed out",
|
|
page.locator("#bsel-c0 option[disabled]").count() == 1
|
|
and page.locator("#bcards .bcard .f .switch[disabled]").count() == 2)
|
|
check("...and the focus stays on the control that was used",
|
|
page.evaluate("document.activeElement.id") == "bsel-c0")
|
|
check("...with 0 page errors", not errors)
|
|
ctx.close()
|
|
# Settings, Simple: the same switch, and the Installer view is one tap.
|
|
ctx, page, errors = open_page(browser, "modes", "#settings",
|
|
size="phone", scheme="dark", lang="nl-BE")
|
|
sent = posts(page)
|
|
page.locator("#bsw-s1").click()
|
|
page.wait_for_timeout(1500)
|
|
check("Simple (NL, phone, dark): B's switch posts pack_use true and reads "
|
|
"Aan", sent[-1:] == [{"action": "pack_use", "unit": "Battery B",
|
|
"value": True}]
|
|
and page.locator("#bsw-s1").get_attribute("aria-checked") == "true")
|
|
page.locator("#set-view [data-v=installer]").click()
|
|
page.wait_for_timeout(500)
|
|
check("...Installer shows the groups and hides Simple",
|
|
page.locator("#groups").is_visible()
|
|
and page.locator("#simple").is_hidden())
|
|
check("...with 0 page errors", not errors)
|
|
ctx.close()
|
|
|
|
|
|
def modes_off(browser):
|
|
ctx, page, errors = open_page(browser, "running", "#batteries",
|
|
size="phone", scheme="light", lang="en-GB")
|
|
page.locator("#batt-off a").click()
|
|
page.wait_for_timeout(1500)
|
|
check("modes off: the card's button opens Settings on the Installer view",
|
|
page.url.endswith("#settings") and page.locator("#groups")
|
|
.is_visible() and not errors)
|
|
ctx.close()
|
|
|
|
|
|
def month_peak(browser):
|
|
# The fixture has no month peak yet, so give /status one on the way in.
|
|
ctx = browser.new_context(locale="en-GB", viewport=SIZES["desktop"])
|
|
page = ctx.new_page()
|
|
|
|
def with_peak(route):
|
|
r = route.fetch()
|
|
body = r.json()
|
|
body["month_peak"]["peak_w"] = 2860.0
|
|
route.fulfill(response=r, json=body)
|
|
page.route("**/status*", with_peak)
|
|
page.goto(url("modes") + "#settings")
|
|
page.wait_for_load_state("networkidle")
|
|
page.wait_for_timeout(1500)
|
|
check("Simple: the peak card shows this month's peak from /status",
|
|
"This month's peak so far: 2.9 kW." in page.locator("#simple")
|
|
.inner_text())
|
|
ctx.close()
|
|
|
|
|
|
def main():
|
|
with sync_playwright() as p:
|
|
b = p.chromium.launch()
|
|
sweep(b)
|
|
actions(b)
|
|
month_peak(b)
|
|
modes_off(b)
|
|
b.close()
|
|
if FAILS:
|
|
print(f"{len(FAILS)} FAILED")
|
|
sys.exit(1)
|
|
print("all passed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|