"""Vào màn GraphRAG phải có báo "đang tải". ``prewarm()`` chỉ chạy 3 giây sau khi cửa sổ hiện (``main_window.py``), nên bấm GraphRAG trong 3 giây đầu sẽ gặp ``_ensure_web()`` dựng ``QWebEngineView`` ĐỒNG BỘ trên GUI thread — đóng băng 1-2 giây mà trước đây không có gì báo. Vì GUI thread bị chặn, panel phải được ``repaint()`` NGAY, không phải ``update()``: ``update()`` chỉ xếp hàng một lượt vẽ cho vòng lặp sự kiện, mà vòng lặp đó sắp bị chặn — panel sẽ chỉ hiện ra sau khi hết đóng băng, đúng lúc không còn cần nữa. """ from __future__ import annotations import pytest pytest.importorskip("PySide6", reason="cần PySide6 để dựng widget thật") class _Renderer: """Thay GraphRenderer. Ghi lại panel có đang hiện lúc nó bị gọi hay không.""" def __init__(self, qapp, web=None, path=""): from PySide6.QtCore import QObject, Signal class _Sig(QObject): graph_rendered = Signal() project_changed = Signal() self._sig = _Sig() self.graph_rendered = self._sig.graph_rendered self.project_changed = self._sig.project_changed self._hien = False self.web = web self.path_edit = type("E", (), {"text": lambda _s: path})() self.busy_luc_duoc_goi = None self._view = None def isVisible(self): # noqa: N802 — khớp API của QWidget return self._hien def auto_scan_and_fit(self): # Chốt điều quan trọng nhất: panel phải hiện TRƯỚC khi việc chặn bắt đầu. self.busy_luc_duoc_goi = not self._view._busy.isHidden() @pytest.fixture def view(qapp): from cowork_local.presentation.graph.structure_graph_view import StructureGraphView return StructureGraphView def _dung(qapp, ViewCls, web=None, path="C:/tmp"): """StructureGraphView với renderer bị thay — renderer thật kéo theo QtWebEngine.""" v = ViewCls.__new__(ViewCls) from PySide6.QtWidgets import QWidget QWidget.__init__(v) v.renderer = _Renderer(qapp, web=web, path=path) v.renderer._view = v v._workspace_project = None v.resize(800, 600) v._build_busy_panel() return v # ---- panel bật trước khi việc chặn bắt đầu ------------------------------- def test_panel_hien_TRUOC_khi_dung_khung_xem(qapp, view): v = _dung(qapp, view) try: v.auto_scan_and_fit() assert v.renderer.busy_luc_duoc_goi is True, ( "panel bật sau khi GUI thread đã bị chặn thì người dùng không thấy gì") finally: v.deleteLater() def test_chua_dung_khung_xem_thi_bao_dang_tai_khung(qapp, view): """Hai giai đoạn, hai thông báo khác nhau.""" from cowork_local.i18n import tr v = _dung(qapp, view, web=None) try: v.auto_scan_and_fit() assert v._busy_label.text() == tr("structure.loading_view") finally: v.deleteLater() def test_da_co_khung_xem_thi_bao_dang_quet(qapp, view): from cowork_local.i18n import tr v = _dung(qapp, view, web=object()) try: v.auto_scan_and_fit() assert v._busy_label.text() == tr("structure.scanning") finally: v.deleteLater() # ---- panel tắt đúng lúc -------------------------------------------------- def test_panel_tat_khi_do_thi_ve_xong(qapp, view): """Tắt theo ``graph_rendered``, không tắt ngay khi hàm trả về — lượt quét còn chạy ở luồng nền phía sau.""" v = _dung(qapp, view) try: v.auto_scan_and_fit() assert v._busy.isHidden() is False v.renderer._sig.graph_rendered.emit() assert v._busy.isHidden() is True finally: v.deleteLater() def test_chua_chon_thu_muc_thi_panel_khong_treo_lai(qapp, view): """Không có gì để vẽ thì ``graph_rendered`` không phát — panel phải tự dọn.""" v = _dung(qapp, view, path="") try: v.auto_scan_and_fit() assert v._busy.isHidden() is True, "panel treo lại mãi mãi" finally: v.deleteLater() # ---- panel nằm giữa và theo kích thước ---------------------------------- def test_panel_nam_giua_khung(qapp, view): v = _dung(qapp, view) try: v.auto_scan_and_fit() g = v._busy.geometry() assert abs(g.center().x() - v.rect().center().x()) <= 1 assert abs(g.center().y() - v.rect().center().y()) <= 1 finally: v.deleteLater() def test_doi_kich_thuoc_thi_panel_theo(qapp, view): """Gửi ``QResizeEvent`` thật thay vì chỉ gọi ``resize()``. Widget chưa ``show()`` thì Qt hoãn việc phát ``resizeEvent`` — mà việc Qt phát event là phần của Qt, không phải phần cần kiểm. Cái cần kiểm là hàm xử lý event có căn lại panel hay không. """ from PySide6.QtCore import QSize from PySide6.QtGui import QResizeEvent v = _dung(qapp, view) try: v.auto_scan_and_fit() cu = v.size() v.resize(1200, 900) v.resizeEvent(QResizeEvent(QSize(1200, 900), cu)) g = v._busy.geometry() assert abs(g.center().x() - v.rect().center().x()) <= 1 assert abs(g.center().y() - v.rect().center().y()) <= 1 finally: v.deleteLater() # ---- màu lấy từ theme, không hardcode ------------------------------------ def test_panel_duoc_style_bang_token_trong_theme(): """Guardrail G4: ngoài theme/ không file nào được đặt tên một màu.""" from pathlib import Path from cowork_local.theme.qss import _TEMPLATE assert "QWidget#graphBusy" in _TEMPLATE.template src = (Path(__file__).resolve().parents[2] / "presentation" / "graph" / "structure_graph_view.py").read_text(encoding="utf-8") assert "setStyleSheet" not in src # ---- đổi project ngay trên màn này thì quét luôn, có panel --------------- def test_doi_project_khi_man_dang_mo_thi_panel_bat(qapp, view): """Renderer quét ngay (không chờ lần ghé sau) nên panel phải bật theo.""" v = _dung(qapp, view) try: v.renderer._hien = True v.renderer.project_changed.emit() assert v._busy.isHidden() is False finally: v.deleteLater() def test_doi_project_khi_man_dang_an_thi_khong_bat_panel(qapp, view): """Renderer hoãn lượt quét sang lần ghé sau — bật panel lúc này là treo lại.""" v = _dung(qapp, view) try: v.renderer._hien = False v.renderer.project_changed.emit() assert v._busy.isHidden() is True finally: v.deleteLater()