]*\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'
]*\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'
]*\bclass="[^"]*\bqb-an\b[^"]*".*?
\s*
',
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"