Files
PacketStation/tests/server/test_search.py
T
spencermandClaude Fable 5.1 6b6db0230f
ci / frontend (push) Successful in 43s
ci / server (push) Successful in 11m6s
ci / image (push) Successful in 38s
ci / e2e (push) Successful in 3m55s
Phase 8: Project Management, find bar, presence and motion
The Project Management screen (/pm, view only) with Overview, Projects & units, Bottlenecks & aging, Hours vs target,
Quality & issues and Find jobs, the period picker, health per unit and per project, drill down, Excel (CSV) per table
and for all, and the PESCO production status report PDF with the controlled-copy stamp; every export logged as
"report exported". The find bar on every home screen: the full search set, the dropdowns, quick flags with Mine, the
flag kinds, Board and List with sorts, filters saved per browser; every filter applied by the server on the list and
the boards, with facets. Presence in packets with role, lock and revoke drop; the motion switch with reduced motion
honoured, card glides, glow and count-up; Back restores the scroll spot; sheet placement and the iPad layout pass. The
data book compared page by page with the old build and six layout differences fixed. Tests for the PM aggregates,
search, filters, presence and exports; Playwright scenarios for the PM screen, the find bar and presence at desktop and
iPad viewports. Docs updated; PLAN.md questions 38 to 42.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-18 15:08:34 -06:00

229 lines
8.6 KiB
Python

"""Phase 8: the find bar search (`q=`), one test per field of the vocabulary, on `GET /api/packets`.
Every word must match some field; a word matches by substring, case insensitive. Each distinctive value below is
on one packet only, so a search returns exactly the packets that carry it. The world is built once per module.
"""
from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass, field
from typing import Any
import pytest
from pmhelpers import add_alias, make_project, make_unit, module_shop, quick_draft
from pshelpers import needs_pg
from shophelpers import Shop, ok, run, step
pytestmark = needs_pg
LONG = "Q" * 60 # a title of exactly 60 characters
@dataclass
class SearchWorld:
shop: Shop
k: dict[str, int] = field(default_factory=dict)
every: set[int] = field(default_factory=set)
def ids(self, *keys: str) -> set[int]:
return {self.k[x] for x in keys}
def make(
shop: Shop,
w: SearchWorld,
key: str,
unit_id: int,
title: str,
*,
ready_dept: str | None = None,
**over: Any,
) -> dict[str, Any]:
pkt = quick_draft(shop, unit_id, title, n=len(w.k) + 1, send=ready_dept, **over)
w.k[key] = int(pkt["id"])
w.every.add(int(pkt["id"]))
return pkt
@pytest.fixture(scope="module")
def world(tmp_path_factory: pytest.TempPathFactory) -> Iterator[SearchWorld]:
with module_shop(tmp_path_factory.mktemp("search")) as shop:
s = shop.settings
w = SearchWorld(shop)
p1 = make_project(s, "L489", "BPX separator", customer="BPX Energy", po="PO-4471")
add_alias(s, p1, "OLD489X")
u52, u53, u54 = (make_unit(s, p1, lot) for lot in ("052", "053", "054"))
p2 = make_project(s, "L512", "Pump skid", customer="Vortex Oil", po="PO-9")
u2 = make_unit(s, p2, "001")
make(shop, w, "title", u52, "Inlet spool", drawing="D-48810", rev="B")
make(shop, w, "op", u53, "Bravo spool", op="HYDRO-TEST")
make(shop, w, "wps", u53, "Charlie spool", wps="WPS-77X")
make(shop, w, "cust", u54, "Delta spool", customer="Acme Gas")
make(
shop,
w,
"wo",
u54,
"Echo spool",
work_orders=[{"wo_base": "5ZZQ", "lot": "007", "split": "0", "sub": "0", "qty": 1, "target_hrs": 1}],
)
make(
shop,
w,
"heat",
u52,
"Foxtrot spool",
bom=[
{"item": 1, "qty": 1, "part_id": "FLG-9876", "description": "Weld neck flange"},
{"item": 2, "qty": 1, "part_id": "FLG-9877", "description": "Blind flange"},
],
)
make(
shop,
w,
"mat",
u53,
"Golf spool",
bom=[
{"item": 1, "qty": 2, "part_id": "GSK-1", "description": "Spiral wound gasket", "material": "A182-F316"}
],
)
make(shop, w, "p2", u2, "Hotel spool")
make(shop, w, "asg", u2, "India spool", ready_dept="03")
make(shop, w, "co", u52, "Juliet spool")
make(shop, w, "dept", u2, "Kilo spool", ready_dept="06")
make(shop, w, "flag", u54, "Lima spool")
make(shop, w, "long", u54, LONG)
# the assigned welder and the stamp
asg = ok(shop("sup.wes").get(f"/api/packets/{w.k['asg']}"))
ok(
step(
shop("sup.wes"), asg, "assign", station_id=shop.stall("03", "Stall 2"), welder_id=shop.uid("weld.andy")
)
)
run(s, "UPDATE packets SET welder_stamp = 'A7Q' WHERE id = %s", (w.k["asg"],))
run(s, "UPDATE users SET welder_stamp = 'A7Q' WHERE sam_account = 'weld.andy'")
# two BOM lines with one heat and two welds with one stamp: one packet, never two rows
run(s, "UPDATE bom_lines SET heat = 'HX44921' WHERE packet_id = %s", (w.k["heat"],))
for no in (1, 2):
run(
s,
"INSERT INTO welds (packet_id, weld_no, items, type, stamp, welder_id) VALUES (%s, %s, '{1}',"
" 'Fillet', 'ZQ9', (SELECT id FROM users WHERE sam_account = 'lead.lou'))",
(w.k["heat"], no),
)
# a change order with an ECN
run(
s,
"INSERT INTO change_orders (packet_id, co_no, origin, review, minor, ecn, reason)"
" VALUES (%s, 47, 'manual', 'approved', true, 'ECN-7781', 'Nozzle moved')",
(w.k["co"],),
)
# a VISUAL draft with a flag kind
run(
s,
"UPDATE packets SET source_kind = 'visual', flags = '{rev_mismatch}', drawing_state = 'rev_mismatch'"
" WHERE id = %s",
(w.k["flag"],),
)
yield w
def found(w: SearchWorld, q: str, sam: str = "sup.wes") -> list[int]:
r = w.shop(sam).get("/api/packets", params={"q": q})
assert r.status_code == 200, r.text
return [int(c["id"]) for c in r.json()["packets"]]
def hits(w: SearchWorld, q: str) -> set[int]:
got = found(w, q)
assert len(got) == len(set(got)), ("duplicate rows", q, got)
return set(got)
P1 = ("title", "op", "wps", "cust", "wo", "heat", "mat", "co", "flag", "long")
P2 = ("p2", "asg", "dept")
DRAFTS = tuple(k for k in (*P1, *P2) if k not in ("asg", "dept"))
@pytest.mark.parametrize(
("q", "keys"),
[
("L512", P2), # project number
("pump", P2), # project name
("L512001", P2), # unit number
("L5121", P2), # unit number with the lot's zeros stripped (L512 || 1)
("L48952", ("title", "heat", "co")), # the same for lot 052
("lot 52", ("title", "heat", "co")), # lot 052 by its number
("HYDRO-TEST", ("op",)), # operation
("D-48810", ("title",)), # drawing
("D-48810 rev B", ("title",)), # drawing and rev
("WPS-77X", ("wps",)), # WPS
("acme", ("cust",)), # packet customer
("vortex", P2), # project customer
("4471", P1), # project PO
("OLD489X", P1), # project alias
("5zzq", ("wo",)), # work order base
("5ZZQ/007", ("wo",)), # work order and lot
("hx449", ("heat",)), # BOM heat
("FLG-9876", ("heat",)), # BOM part id
("spiral", ("mat",)), # BOM description
("A182-F316", ("mat",)), # BOM material
("andy", ("asg",)), # assigned welder's name
("A7Q", ("asg",)), # assigned welder's stamp
("ZQ9", ("heat",)), # weld stamp
("lou", ("heat",)), # the welder named on a weld
("CO-47", ("co",)), # change order number
("ECN-7781", ("co",)), # ECN
("stall", ("asg",)), # station name
("bldg", ("dept",)), # department name
("sent", ("asg",)), # status label: assigned is "Sent to station"
("rev_mismatch", ("flag",)), # flag kind
("flagged", DRAFTS), # every flagged draft (hand drafts and the flagged VISUAL draft)
("inlet", ("title",)), # title
("INLET", ("title",)), # case insensitive
],
)
def test_search_field(world: SearchWorld, q: str, keys: tuple[str, ...]) -> None:
assert hits(world, q) == world.ids(*keys)
def test_lot_and_rev_alone_match_everything(world: SearchWorld) -> None:
assert hits(world, "lot") == world.every
assert hits(world, "rev") == world.every
assert hits(world, "lot 052") == world.ids("title", "heat", "co")
def test_several_words_narrow(world: SearchWorld) -> None:
assert hits(world, "L489") == world.ids(*P1)
assert hits(world, "L489 spool") == world.ids(*(k for k in P1 if k != "long"))
assert hits(world, "L489 52") == world.ids("title", "heat", "co")
assert hits(world, "L489 52 inlet") == world.ids("title")
assert hits(world, "L512 HYDRO-TEST") == set()
assert hits(world, " inlet L489 ") == world.ids("title")
def test_no_duplicates_from_child_rows(world: SearchWorld) -> None:
for q in ("hx449", "ZQ9", "FLG", "lou", "flange"):
got = found(world, q)
assert got == [world.k["heat"]], (q, got)
def test_long_word_is_cut_at_60(world: SearchWorld) -> None:
assert hits(world, LONG) == world.ids("long")
assert hits(world, LONG + "ZZZZ") == world.ids("long") # cut to the first 60 characters
assert hits(world, "Q" * 61) == world.ids("long")
def test_only_eight_words_count(world: SearchWorld) -> None:
assert hits(world, " ".join(["inlet"] * 8)) == world.ids("title")
assert hits(world, " ".join(["lot"] * 8 + ["inlet"])) == world.every
def test_like_wildcards_are_literal(world: SearchWorld) -> None:
assert hits(world, "%") == set()
assert hits(world, "_") == world.ids("flag") # only the flag kind has an underscore