Food Desert commited on
Commit
0cd97fa
·
1 Parent(s): 56966b7

Fix tag source color mapping and add UI color smoke checks

Browse files
Files changed (2) hide show
  1. app.py +34 -6
  2. scripts/smoke_ui_state.py +81 -0
app.py CHANGED
@@ -749,13 +749,25 @@ def _build_tooltip_payload(row_defs: List[Dict[str, Any]], max_rows: int) -> str
749
  row_defs_ui = (row_defs or [])[: max(0, int(max_rows))]
750
  tips: Dict[str, str] = {}
751
  rows: List[List[str]] = []
 
752
  for row in row_defs_ui:
753
  tags = _dedupe_norm_tags(row.get("tags", []) if isinstance(row, dict) else [])
754
  rows.append(tags)
 
 
 
 
 
 
 
 
 
 
 
755
  for t in tags:
756
  if t not in tips:
757
  tips[t] = _tooltip_text_for_tag(t)
758
- return json.dumps({"rows": rows, "tips": tips}, ensure_ascii=True)
759
 
760
 
761
  def _build_row_component_updates(
@@ -1621,32 +1633,48 @@ client_js = """
1621
 
1622
  const readTooltipMap = () => {
1623
  const el = document.querySelector("#psq-tooltip-map textarea, #psq-tooltip-map input");
1624
- if (!el) return { rows: [], tips: {} };
1625
  const raw = (el.value || "").trim();
1626
- if (!raw) return { rows: [], tips: {} };
1627
  try {
1628
  const obj = JSON.parse(raw);
1629
- if (!obj || typeof obj !== "object") return { rows: [], tips: {} };
1630
  const rows = Array.isArray(obj.rows) ? obj.rows : [];
 
1631
  const tips = (obj.tips && typeof obj.tips === "object") ? obj.tips : {};
1632
- return { rows, tips };
1633
  } catch (_) {
1634
- return { rows: [], tips: {} };
1635
  }
1636
  };
1637
 
1638
  const applyTooltips = () => {
1639
  const payload = readTooltipMap();
1640
  const rowTags = Array.isArray(payload.rows) ? payload.rows : [];
 
1641
  const tipMap = (payload.tips && typeof payload.tips === "object") ? payload.tips : {};
 
1642
  const rowEls = document.querySelectorAll(".lego-tags");
1643
  rowEls.forEach((rowEl, rowIdx) => {
1644
  const tags = Array.isArray(rowTags[rowIdx]) ? rowTags[rowIdx] : [];
 
1645
  const labels = rowEl.querySelectorAll("label");
1646
  labels.forEach((label, tagIdx) => {
1647
  const span = label.querySelector("span");
1648
  const tag = (tagIdx < tags.length) ? tags[tagIdx] : "";
1649
  const tip = tag && Object.prototype.hasOwnProperty.call(tipMap, tag) ? (tipMap[tag] || "") : "";
 
 
 
 
 
 
 
 
 
 
 
 
1650
  if (tip) {
1651
  label.title = tip;
1652
  if (span) span.title = tip;
 
749
  row_defs_ui = (row_defs or [])[: max(0, int(max_rows))]
750
  tips: Dict[str, str] = {}
751
  rows: List[List[str]] = []
752
+ meta_rows: List[List[Dict[str, Any]]] = []
753
  for row in row_defs_ui:
754
  tags = _dedupe_norm_tags(row.get("tags", []) if isinstance(row, dict) else [])
755
  rows.append(tags)
756
+ row_meta = row.get("tag_meta", {}) if isinstance(row, dict) and isinstance(row.get("tag_meta", {}), dict) else {}
757
+ meta_row: List[Dict[str, Any]] = []
758
+ for t in tags:
759
+ raw_meta = row_meta.get(t, {}) if isinstance(row_meta.get(t, {}), dict) else {}
760
+ meta_row.append(
761
+ {
762
+ "origin": _normalize_selection_origin(str(raw_meta.get("origin", "selection"))),
763
+ "preselected": bool(raw_meta.get("preselected", False)),
764
+ }
765
+ )
766
+ meta_rows.append(meta_row)
767
  for t in tags:
768
  if t not in tips:
769
  tips[t] = _tooltip_text_for_tag(t)
770
+ return json.dumps({"rows": rows, "meta_rows": meta_rows, "tips": tips}, ensure_ascii=True)
771
 
772
 
773
  def _build_row_component_updates(
 
1633
 
1634
  const readTooltipMap = () => {
1635
  const el = document.querySelector("#psq-tooltip-map textarea, #psq-tooltip-map input");
1636
+ if (!el) return { rows: [], meta_rows: [], tips: {} };
1637
  const raw = (el.value || "").trim();
1638
+ if (!raw) return { rows: [], meta_rows: [], tips: {} };
1639
  try {
1640
  const obj = JSON.parse(raw);
1641
+ if (!obj || typeof obj !== "object") return { rows: [], meta_rows: [], tips: {} };
1642
  const rows = Array.isArray(obj.rows) ? obj.rows : [];
1643
+ const meta_rows = Array.isArray(obj.meta_rows) ? obj.meta_rows : [];
1644
  const tips = (obj.tips && typeof obj.tips === "object") ? obj.tips : {};
1645
+ return { rows, meta_rows, tips };
1646
  } catch (_) {
1647
+ return { rows: [], meta_rows: [], tips: {} };
1648
  }
1649
  };
1650
 
1651
  const applyTooltips = () => {
1652
  const payload = readTooltipMap();
1653
  const rowTags = Array.isArray(payload.rows) ? payload.rows : [];
1654
+ const rowMeta = Array.isArray(payload.meta_rows) ? payload.meta_rows : [];
1655
  const tipMap = (payload.tips && typeof payload.tips === "object") ? payload.tips : {};
1656
+ const validOrigins = new Set(["rewrite", "selection", "probe", "structural", "implied", "user"]);
1657
  const rowEls = document.querySelectorAll(".lego-tags");
1658
  rowEls.forEach((rowEl, rowIdx) => {
1659
  const tags = Array.isArray(rowTags[rowIdx]) ? rowTags[rowIdx] : [];
1660
+ const metas = Array.isArray(rowMeta[rowIdx]) ? rowMeta[rowIdx] : [];
1661
  const labels = rowEl.querySelectorAll("label");
1662
  labels.forEach((label, tagIdx) => {
1663
  const span = label.querySelector("span");
1664
  const tag = (tagIdx < tags.length) ? tags[tagIdx] : "";
1665
  const tip = tag && Object.prototype.hasOwnProperty.call(tipMap, tag) ? (tipMap[tag] || "") : "";
1666
+ const meta = (tagIdx < metas.length && metas[tagIdx] && typeof metas[tagIdx] === "object")
1667
+ ? metas[tagIdx]
1668
+ : {};
1669
+ const originRaw = String(meta.origin || "selection").trim().toLowerCase();
1670
+ const origin = validOrigins.has(originRaw) ? originRaw : "selection";
1671
+ const preselected = !!meta.preselected;
1672
+ label.setAttribute("data-psq-origin", origin);
1673
+ label.setAttribute("data-psq-preselected", preselected ? "1" : "0");
1674
+ if (span) {
1675
+ span.setAttribute("data-psq-origin", origin);
1676
+ span.setAttribute("data-psq-preselected", preselected ? "1" : "0");
1677
+ }
1678
  if (tip) {
1679
  label.title = tip;
1680
  if (span) span.title = tip;
scripts/smoke_ui_state.py CHANGED
@@ -1,4 +1,6 @@
1
  import sys
 
 
2
  from pathlib import Path
3
 
4
  sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
@@ -10,6 +12,82 @@ def _assert(cond: bool, msg: str) -> None:
10
  raise AssertionError(msg)
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def test_prompt_uses_visible_rows_only() -> None:
14
  # If selected state contains stale hidden tags, prompt should still reflect visible-row selections only.
15
  row_defs = [
@@ -183,6 +261,9 @@ def test_shared_tag_mirrors_without_unrelated_cross_toggle() -> None:
183
 
184
 
185
  def main() -> None:
 
 
 
186
  test_prompt_uses_visible_rows_only()
187
  test_row_deduping()
188
  test_rebuild_ignores_stale_selected_state()
 
1
  import sys
2
+ import re
3
+ import json
4
  from pathlib import Path
5
 
6
  sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
 
12
  raise AssertionError(msg)
13
 
14
 
15
+ def _css_block(selector: str) -> str:
16
+ pat = re.compile(rf"{re.escape(selector)}\s*\{{([^}}]+)\}}", re.DOTALL)
17
+ m = pat.search(app.css)
18
+ _assert(m is not None, f"missing CSS selector block: {selector}")
19
+ return m.group(1)
20
+
21
+
22
+ def _css_prop(block: str, prop: str) -> str:
23
+ pat = re.compile(rf"{re.escape(prop)}\s*:\s*([^;]+);", re.IGNORECASE)
24
+ m = pat.search(block)
25
+ _assert(m is not None, f"missing CSS property '{prop}' in block: {block}")
26
+ return m.group(1).strip().lower()
27
+
28
+
29
+ def test_toggle_color_contract_matches_legend() -> None:
30
+ legend_bg_by_origin = {
31
+ "rewrite": _css_prop(_css_block(".source-legend .chip.rewrite"), "background"),
32
+ "selection": _css_prop(_css_block(".source-legend .chip.selection"), "background"),
33
+ "probe": _css_prop(_css_block(".source-legend .chip.probe"), "background"),
34
+ "structural": _css_prop(_css_block(".source-legend .chip.structural"), "background"),
35
+ "implied": _css_prop(_css_block(".source-legend .chip.implied"), "background"),
36
+ "user": _css_prop(_css_block(".source-legend .chip.user"), "background"),
37
+ }
38
+
39
+ on_bg2_by_origin = {
40
+ "rewrite": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="rewrite"] span'), "--on-bg2"),
41
+ "selection": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="selection"] span'), "--on-bg2"),
42
+ "probe": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="probe"] span'), "--on-bg2"),
43
+ "structural": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="structural"] span'), "--on-bg2"),
44
+ "implied": _css_prop(_css_block('.lego-tags label[data-psq-preselected="1"][data-psq-origin="implied"] span'), "--on-bg2"),
45
+ "user": _css_prop(_css_block('.lego-tags label[data-psq-preselected="0"] span'), "--on-bg2"),
46
+ }
47
+
48
+ for origin, legend_bg in legend_bg_by_origin.items():
49
+ _assert(
50
+ on_bg2_by_origin[origin] == legend_bg,
51
+ f"legend/toggle color mismatch for '{origin}': toggle={on_bg2_by_origin[origin]} legend={legend_bg}",
52
+ )
53
+
54
+
55
+ def test_client_js_stamps_source_color_attributes() -> None:
56
+ js = app.client_js
57
+ has_origin_attr = ("data-psq-origin" in js) or ("dataset.psqOrigin" in js)
58
+ has_preselected_attr = ("data-psq-preselected" in js) or ("dataset.psqPreselected" in js)
59
+ _assert(has_origin_attr, "client_js does not stamp per-tag origin attributes on checkbox labels")
60
+ _assert(has_preselected_attr, "client_js does not stamp per-tag preselected attributes on checkbox labels")
61
+
62
+
63
+ def test_tooltip_payload_includes_row_meta_for_color_mapping() -> None:
64
+ row_defs = [
65
+ {
66
+ "name": "r1",
67
+ "label": "R1",
68
+ "tags": ["solo", "female"],
69
+ "tag_meta": {
70
+ "solo": {"origin": "rewrite", "preselected": True},
71
+ "female": {"origin": "selection", "preselected": False},
72
+ },
73
+ }
74
+ ]
75
+ raw = app._build_tooltip_payload(row_defs, max_rows=app.display_max_rows_default)
76
+ payload = json.loads(raw)
77
+ _assert("meta_rows" in payload, "tooltip payload missing meta_rows")
78
+ _assert(isinstance(payload["meta_rows"], list), "tooltip payload meta_rows must be a list")
79
+ _assert(len(payload["meta_rows"]) == 1, "meta_rows should align with visible row count")
80
+ _assert(isinstance(payload["meta_rows"][0], list), "meta_rows row must be a list")
81
+ _assert(len(payload["meta_rows"][0]) == 2, "meta_rows tag metadata count must align with tags")
82
+
83
+ first = payload["meta_rows"][0][0]
84
+ second = payload["meta_rows"][0][1]
85
+ _assert(first.get("origin") == "rewrite", "first tag origin metadata mismatch")
86
+ _assert(first.get("preselected") is True, "first tag preselected metadata mismatch")
87
+ _assert(second.get("origin") == "selection", "second tag origin metadata mismatch")
88
+ _assert(second.get("preselected") is False, "second tag preselected metadata mismatch")
89
+
90
+
91
  def test_prompt_uses_visible_rows_only() -> None:
92
  # If selected state contains stale hidden tags, prompt should still reflect visible-row selections only.
93
  row_defs = [
 
261
 
262
 
263
  def main() -> None:
264
+ test_toggle_color_contract_matches_legend()
265
+ test_client_js_stamps_source_color_attributes()
266
+ test_tooltip_payload_includes_row_meta_for_color_mapping()
267
  test_prompt_uses_visible_rows_only()
268
  test_row_deduping()
269
  test_rebuild_ignores_stale_selected_state()