Spaces:
Sleeping
Sleeping
File size: 10,992 Bytes
78738de | 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 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | """Contract cách ly tab trên MOBILE (14/08/2026 — bàn giao 2).
Bug Danh thấy trên điện thoại thật (bản Space vừa deploy): mở app vào thẳng
tab Analyzer — tab ĐẦU + MẶC ĐỊNH từ ``3740e17`` — mà đáy màn hình vẫn là UI
tab Gợi ý (badge bi mục tiêu, chấm đầu cơ, thanh lực, nút Gợi ý/Đánh).
Nguyên nhân: ``#quickbar`` nằm NGOÀI ``#panel``. ``setTab()`` chỉ ẩn/hiện các
section BÊN TRONG ``#panel`` (``.controls``/``.legend``/``#drills``/
``#analyzer``) nên không có gì với tới quickbar. Desktop không lộ vì
``#quickbar`` là ``display:none`` ngoài media query mobile — đúng vế "desktop
không bị" trong báo cáo.
Hai vế bị khoá ở đây, vì mỗi vế hỏng một kiểu:
* **Trạng thái TĨNH lúc boot.** ``setTab()`` KHÔNG chạy khi mở app bình
thường — nó chỉ chạy khi bấm tab, hoặc khi deep-link ``?qr=`` gọi
``exitAnalyzer()``. Trạng thái đầu do chính HTML tĩnh mang (nếp
``.controls``/``.legend`` có sẵn ``hidden``: MỘT nguồn sự thật, không flash
tab). Sửa mỗi JS là bug còn nguyên ở đúng ca Danh gặp — mở app.
* **Lối ra phải ở lại.** Trên mobile ``#panel`` là ``display:none`` cho tới
khi "Chi tiết ▴" mở sheet. Ẩn cả ``#quickbar`` cho gọn là nhốt người dùng
trong tab hiện tại — không còn nút nào mở panel để đổi tab.
Test đọc FILE THẬT trong ``app/static/`` (nếp ``test_static_cache.py`` /
``test_scan.py``): repo không có JS runtime, nên cái khoá được là markup +
nguồn ``setTab``. Vế mắt người (điện thoại thật) vẫn do smoke lo.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
STATIC = Path(__file__).resolve().parents[1] / "app" / "static"
# UI thuộc tab Gợi ý sống trong quickbar — phải biến mất ở tab Analyzer.
# #qb-target = song sinh của #panel-target trong .controls (badge bi mục tiêu).
QB_SHOT_UI = ["qb-spin", "qb-info", "qb-go", "qb-play"]
# Điều hướng — PHẢI ở lại mọi tab (lối duy nhất mở #panel khi sheet đóng).
QB_NAV = ["qb-drills", "qb-detail"]
@pytest.fixture(scope="module")
def html() -> str:
return (STATIC / "index.html").read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def js() -> str:
return (STATIC / "app.js").read_text(encoding="utf-8")
def _quickbar(html: str) -> str:
m = re.search(r'<nav id="quickbar">(.*?)</nav>', html, re.S)
assert m, "không thấy <nav id=\"quickbar\"> trong index.html"
return m.group(1)
def _classes(block: str, tag: str, el_id: str) -> set[str]:
m = re.search(rf'<{tag}[^>]*\bid="{el_id}"[^>]*>', block)
assert m, f"không thấy #{el_id} trong quickbar"
cls = re.search(r'class="([^"]*)"', m.group(0))
return set(cls.group(1).split()) if cls else set()
def _settab_body(js: str, *, code_only: bool = False) -> str:
m = re.search(r"function setTab\(mode\) \{(.*?)\n\}", js, re.S)
assert m, "không thấy function setTab(mode) trong app.js"
body = m.group(1)
if code_only:
# bỏ comment: thân hàm giải thích vì sao KHÔNG đụng .qb-settings, mà
# chính lời giải thích đó lại chứa tên id đang muốn kiểm là vắng mặt
body = re.sub(r"//[^\n]*", "", body)
return body
def test_boot_tinh_an_san_ui_goi_y_cua_quickbar(html):
"""Boot = tab Analyzer, mà setTab() không chạy lúc mở app → markup tĩnh
phải tự mang trạng thái ẩn, y như .controls/.legend."""
qb = _quickbar(html)
assert "hidden" in _classes(qb, "span", "qb-target"), \
"#qb-target thiếu .hidden ở markup tĩnh — mở app trên mobile là badge " \
"bi mục tiêu của tab Gợi ý hiện ngay giữa tab Analyzer"
m = re.search(r'<div[^>]*\bclass="([^"]*\bqb-main\b[^"]*)"', qb)
assert m, "không thấy hàng .qb-main trong quickbar"
assert "hidden" in m.group(1).split(), \
"hàng .qb-main thiếu .hidden ở markup tĩnh — spin/lực/Gợi ý/Đánh hiện " \
"ngay lúc mở app ở tab Analyzer (đúng bug Danh báo 14/08)"
def test_loi_ra_cua_quickbar_khong_bao_gio_bi_an(html, js):
"""Trên mobile #panel là display:none tới khi sheet mở — hàng .qb-settings
là lối duy nhất mở panel để đổi tab. Ẩn nó là app cụt đường."""
qb = _quickbar(html)
for el_id in QB_NAV:
assert "hidden" not in _classes(qb, "button", el_id), \
f"#{el_id} bị ẩn tĩnh — không còn lối mở #panel trên mobile"
body = _settab_body(js, code_only=True)
for token in ("qb-settings", *QB_NAV):
assert token not in body, \
f"setTab() đụng tới {token} — điều hướng phải sống ở mọi tab"
def test_settab_an_hien_ui_goi_y_theo_tab(js):
"""Vế động: bấm qua lại tab phải kéo theo quickbar, không chỉ đúng lúc boot."""
body = _settab_body(js)
assert 'mode === "analyzer"' in body
for sel in ('$("#qb-target")', '$(".qb-main")'):
assert f'{sel}.classList.toggle("hidden"' in body, \
f"setTab() không toggle {sel} — đổi tab xong quickbar giữ nguyên"
def test_moi_widget_goi_y_cua_quickbar_nam_trong_khoi_bi_an(html):
"""Khoá cấu trúc cho lần sau: widget gợi ý mới mà lỡ đặt vào hàng
.qb-settings thì nó nằm ngoài khối bị ẩn → bug quay lại y hệt."""
qb = _quickbar(html)
cut = qb.find("qb-main")
assert cut > 0
settings_row, main_row = qb[:cut], qb[cut:]
for el_id in QB_SHOT_UI:
assert f'id="{el_id}"' in main_row, \
f"#{el_id} không nằm trong hàng .qb-main — sẽ không bị ẩn theo tab"
for el_id in QB_NAV:
assert f'id="{el_id}"' in settings_row, \
f"#{el_id} rơi vào .qb-main — lối ra sẽ bị ẩn cùng UI gợi ý"
# ---------------------------------- hàng quickbar của tab Analyzer (14/08)
# Vế ĐỐI XỨNG của mọi test trên: tab Gợi ý giấu UI của nó ở tab Analyzer, thì
# tab Analyzer cũng phải giấu UI của nó ở nơi khác. Hàng .qb-an mang thông số
# cú đang mở + nút đánh lại — chỉ có nghĩa khi ĐANG mở một cú.
QB_AN_UI = ["qban-spin", "qban-info", "qban-replay"]
def test_hang_analyzer_an_san_luc_boot(html):
"""Mở app = tab Analyzer nhưng ở màn DANH SÁCH, chưa mở cú nào → không có
thông số nào để hiện. setTab() không chạy lúc boot nên markup phải tự
mang .hidden (cùng lý do với .qb-main)."""
qb = _quickbar(html)
m = re.search(r'<div[^>]*\bclass="([^"]*\bqb-an\b[^"]*)"', qb)
assert m, "không thấy hàng .qb-an trong quickbar"
assert "hidden" in m.group(1).split(), \
"hàng .qb-an thiếu .hidden ở markup tĩnh — mở app là thấy widget cú " \
"rỗng ở đáy màn hình"
def test_hang_analyzer_khong_muon_class_co_binding(html):
"""Bài học .btn-play: .power-bar/.spin-dot bị updateShotViz quét toàn cục
(tab Gợi ý), .btn-go/.btn-play bị syncButtons ép label + disabled. Hàng
Analyzer PHẢI dùng class riêng, trừ .and-* là cố ý dùng chung với widget
trang chi tiết để một hàm nuôi cả hai."""
qb = _quickbar(html)
m = re.search(r'<div[^>]*\bclass="[^"]*\bqb-an\b[^"]*".*?</div>\s*</div>',
qb, re.S)
assert m, "không cắt được khối .qb-an"
row = m.group(0)
# so theo TOKEN class, không phải substring: "and-power-bar" chứa
# "power-bar" nhưng là class khác hẳn
tokens = {t for attr in re.findall(r'class="([^"]*)"', row)
for t in attr.split()}
for bad in ("power-bar", "spin-dot", "btn-go", "btn-play"):
assert bad not in tokens, \
f"hàng .qb-an mượn .{bad} — sẽ bị quét chéo"
# ...nhưng PHẢI mang .and-* để fillAndShotviz + initPowerBar nuôi nó
assert "and-power-bar" in row and "and-spin-dot" in row, \
"hàng .qb-an không mang class .and-* — thanh lực sẽ không có nấc và " \
"chấm spin không bao giờ được cập nhật"
def test_sync_hang_analyzer_goi_o_moi_cho_trang_thai_doi(js):
"""Hàng chỉ đúng nếu được đồng bộ ở CẢ 4 chỗ: đổi tab, mở cú, đóng cú, và
sau khi animate xong. Thiếu chỗ nào là hàng kẹt lại sai trạng thái."""
assert "function syncQbAnalyzer()" in js
for fn in ("setTab", "renderAnvDetail", "closeAnvDetail", "anvReplay"):
m = re.search(rf"function {fn}\(.*?\n\}}", js, re.S)
assert m, f"không thấy function {fn} trong app.js"
assert "syncQbAnalyzer()" in m.group(0), \
f"{fn}() không gọi syncQbAnalyzer() — hàng quickbar Analyzer sẽ " \
f"kẹt lại trạng thái cũ"
def test_danh_lai_cu_dong_sheet(js):
"""Trên mobile #panel là bottom sheet CHE BÀN — không đóng thì cú chạy
sau lưng người xem. Cả hai nút (trong sheet + quickbar) đi chung
anvReplay nên chỉ cần một chỗ, nhưng phải có."""
m = re.search(r"async function anvReplay\(\) \{(.*?)\n\}", js, re.S)
assert m, "không thấy function anvReplay() trong app.js"
assert "closeSheet();" in m.group(1)
assert '$("#qban-replay").addEventListener("click", anvReplay);' in js
def test_fill_shotviz_nuoi_ca_hai_widget(js):
"""Một nguồn số cho widget trang chi tiết VÀ hàng quickbar — nếu quay lại
ghi theo id đơn lẻ thì hai chỗ hiện hai con số khác nhau."""
m = re.search(r"function fillAndShotviz\(sn\) \{(.*?)\n\}", js, re.S)
assert m, "không thấy function fillAndShotviz trong app.js"
body = m.group(1)
assert 'all(".and-power-bar")' in body and 'all(".and-spin-dot")' in body, \
"fillAndShotviz ghi theo id đơn lẻ — hàng quickbar sẽ không được nuôi"
def test_deep_link_qr_van_dap_xuong_tab_goi_y(js):
"""Hành vi chốt từ A2b: QR dán ở bàn là lối vào luồng gợi ý — deep-link
phải tự thoát tab Analyzer, không bắt người chơi bấm tab.
Liên quan tới quickbar vì nó là đường DUY NHẤT trả UI gợi ý về sau khi
markup tĩnh ẩn sẵn: ``exitAnalyzer() → setTab("suggest")``. Cắt mắt xích
này là ``?qr=`` mở ra một tab Gợi ý cụt quickbar trên mobile."""
assert 'if (new URLSearchParams(location.search).get("qr")) exitAnalyzer();' in js
m = re.search(r"function exitAnalyzer\(\) \{(.*?)\n\}", js, re.S)
assert m, "không thấy function exitAnalyzer() trong app.js"
assert 'setTab("suggest")' in m.group(1), \
"exitAnalyzer() không còn gọi setTab(\"suggest\") — quickbar sẽ kẹt ở " \
"trạng thái ẩn của tab Analyzer"
|