feat(ui): GraphRAG giữ project đã chọn, tự quét, và báo khi đang tải
Cùng lớp lỗi với tab Thư mục: _bind_project gọi _structure.set_project(pid) vô điều kiện, và chuỗi _goto -> refresh -> _load_current -> _bind_project chạy lại mỗi lần vào lại màn Workspace, nên bộ chọn project của chính màn GraphRAG bị kéo về giá trị của Workspace ở mỗi cú chuyển tab. set_workspace_project() bỏ qua khi project không đổi (dùng None làm giá trị khởi tạo, không phải "", để lần gọi đầu không bị bỏ qua khi chưa chọn project nào). Chọn project xong phải tự quét: _on_project_changed cố ý hoãn lượt quét sang "lần ghé tiếp", nhưng combo nằm ngay trên màn đó nên người dùng đang đứng ở đấy và không có lần ghé tiếp nào — họ phải tự bấm Scan. Giờ quét ngay khi màn đang mở; hoãn vẫn giữ cho trường hợp đổi project từ màn Workspace khi GraphRAG ẩn. Panel "đang tải": prewarm() chỉ chạy 3 giây sau khi cửa sổ hiện, nên bấm GraphRAG trong 3 giây đầu gặp _ensure_web() dựng QWebEngineView ĐỒNG BỘ trên GUI thread — đóng băng 1-2 giây mà không có gì báo. Vì GUI thread bị chặn, panel phải repaint() ngay chứ không 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, nên panel sẽ chỉ hiện ra sau khi hết đóng băng — đúng lúc không còn cần tới nó nữa. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
thanhnv
co-authored by
Claude Opus 5
parent
6f122575b0
commit
33876f96f6
@@ -336,6 +336,9 @@ STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"vi": "Nhấp node để mở thư mục, hoặc hỏi agent về đồ thị."},
|
||||
"structure.pick_folder_title": {"en": "Choose folder", "ja": "フォルダを選択", "vi": "Chọn thư mục"},
|
||||
"structure.scanning": {"en": "Scanning structure…", "ja": "構造をスキャン中…", "vi": "Đang quét cấu trúc…"},
|
||||
"structure.loading_view": {
|
||||
"en": "Loading the graph view…", "ja": "グラフビューを読み込み中…",
|
||||
"vi": "Đang tải khung đồ thị…"},
|
||||
"structure.scan_error": {"en": "Scan error: {err}", "ja": "スキャンエラー: {err}", "vi": "Lỗi khi quét: {err}"},
|
||||
"structure.graph_summary": {"en": "Graph: {nodes} nodes, {edges} edges.{note}", "ja": "グラフ: ノード {nodes} 個、エッジ {edges} 個。{note}", "vi": "Đồ thị: {nodes} node, {edges} cạnh.{note}"},
|
||||
"structure.truncated_note": {"en": " (truncated — too many nodes)", "ja": " (切り捨て:ノードが多すぎます)", "vi": " (đã cắt bớt — quá nhiều node)"},
|
||||
|
||||
@@ -202,6 +202,14 @@ class GraphRenderer(QWidget):
|
||||
# Mark it and scan on the next visit rather than now — see
|
||||
# auto_scan_and_fit()'s docstring for why.
|
||||
self._needs_scan = True
|
||||
# ...except when this screen is the one on show. The picker lives HERE,
|
||||
# so a user changing project is already looking at the graph: there is
|
||||
# no "next visit" to defer to, and they had to press Scan by hand.
|
||||
# Deferring still applies when the change came from the Workspace
|
||||
# screen while this one is hidden, which is what it was for.
|
||||
if self.isVisible() and self.path_edit.text().strip():
|
||||
self._needs_scan = False
|
||||
self._scan()
|
||||
|
||||
# ---- helpers ---------------------------------------------------------------- #
|
||||
def _pick(self) -> None:
|
||||
|
||||
@@ -9,9 +9,11 @@ inline in ``ui/structure_graph_view.py::StructureGraphView.__init__`` (lines
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QSplitter, QVBoxLayout, QWidget
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout, QLabel, QProgressBar, QSplitter, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from cowork_local.i18n import on_language_changed
|
||||
from cowork_local.i18n import on_language_changed, tr
|
||||
from cowork_local.presentation.graph.graph_qa_widget import GraphQaWidget
|
||||
from cowork_local.presentation.graph.graph_renderer import GraphRenderer
|
||||
from cowork_local.state import AppContext
|
||||
@@ -35,6 +37,9 @@ class StructureGraphView(QWidget):
|
||||
"""
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
# Project ma man Workspace da ap xuong lan gan nhat. None = chua ap lan
|
||||
# nao, de lan goi dau tien khong bi bo qua ke ca khi pid la chuoi rong.
|
||||
self._workspace_project = None
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
self.renderer = GraphRenderer(ctx)
|
||||
@@ -50,6 +55,7 @@ class StructureGraphView(QWidget):
|
||||
self._split.setSizes(list(_COLLAPSED_SIZES_HINT))
|
||||
root.addWidget(self._split, 1)
|
||||
|
||||
self._build_busy_panel()
|
||||
on_language_changed(self._retranslate)
|
||||
|
||||
def _retranslate(self) -> None:
|
||||
@@ -72,18 +78,116 @@ class StructureGraphView(QWidget):
|
||||
self._split.setSizes(list(_COLLAPSED_SIZES_HINT))
|
||||
|
||||
# ---- public API (app.py / ui/workspace_tab.py) --------------------------- #
|
||||
# ---- panel "đang tải" ---------------------------------------------------
|
||||
def _build_busy_panel(self) -> None:
|
||||
"""Panel phủ lên khung đồ thị trong lúc nó đang được dựng.
|
||||
|
||||
Vì sao cần: ``prewarm()`` chỉ chạy 3 giây sau khi cửa sổ hiện
|
||||
(``main_window.py``), nên người dùng 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.
|
||||
"""
|
||||
self._busy = QWidget(self)
|
||||
self._busy.setObjectName("graphBusy")
|
||||
lay = QHBoxLayout(self._busy)
|
||||
lay.setContentsMargins(18, 14, 18, 14)
|
||||
lay.setSpacing(12)
|
||||
self._busy_label = QLabel()
|
||||
bar = QProgressBar()
|
||||
bar.setRange(0, 0) # chế độ vô định
|
||||
bar.setTextVisible(False)
|
||||
bar.setFixedWidth(120)
|
||||
lay.addWidget(self._busy_label)
|
||||
lay.addWidget(bar)
|
||||
self._busy.hide()
|
||||
self.renderer.graph_rendered.connect(self._hide_busy)
|
||||
# Đổi project ngay trên màn này thì renderer quét luôn, không chờ lần ghé
|
||||
# sau — nên panel phải bật theo, không thì lượt quét đó lại im lặng.
|
||||
self.renderer.project_changed.connect(self._on_project_scan_started)
|
||||
|
||||
def _on_project_scan_started(self) -> None:
|
||||
"""Renderer vừa đổi project. Nó chỉ quét ngay khi màn này đang mở."""
|
||||
if self.renderer.isVisible():
|
||||
self._show_busy("structure.scanning")
|
||||
|
||||
def _show_busy(self, key: str) -> None:
|
||||
"""Hiện panel và ÉP VẼ NGAY.
|
||||
|
||||
``repaint()`` chứ không ``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 tới nó nữa.
|
||||
"""
|
||||
self._busy_label.setText(tr(key))
|
||||
self._busy.adjustSize()
|
||||
self._center_busy()
|
||||
self._busy.show()
|
||||
self._busy.raise_()
|
||||
self._busy.repaint()
|
||||
|
||||
def _hide_busy(self) -> None:
|
||||
"""Ẩn panel khi đồ thị đã vẽ xong."""
|
||||
if getattr(self, "_busy", None) is not None:
|
||||
self._busy.hide()
|
||||
|
||||
def _center_busy(self) -> None:
|
||||
"""Giữ panel ở giữa khung, kể cả khi cửa sổ đổi kích thước."""
|
||||
busy = getattr(self, "_busy", None)
|
||||
if busy is None:
|
||||
return
|
||||
size = busy.sizeHint()
|
||||
busy.setGeometry((self.width() - size.width()) // 2,
|
||||
(self.height() - size.height()) // 2,
|
||||
size.width(), size.height())
|
||||
|
||||
def resizeEvent(self, e): # noqa: N802
|
||||
"""Đổi kích thước cửa sổ thì panel phải theo."""
|
||||
super().resizeEvent(e)
|
||||
self._center_busy()
|
||||
|
||||
def schedule_rescan(self, path: str = "") -> None:
|
||||
"""Hẹn quét lại đồ thị sau khi thư mục có thay đổi."""
|
||||
self.renderer.schedule_rescan(path)
|
||||
|
||||
def auto_scan_and_fit(self) -> None:
|
||||
"""Vào màn GraphRAG: hiện đồ thị, chỉ quét lại khi thật sự cần."""
|
||||
self.renderer.auto_scan_and_fit()
|
||||
"""Vào màn GraphRAG: hiện đồ thị, chỉ quét lại khi thật sự cần.
|
||||
|
||||
Panel "đang tải" bật TRƯỚC khi gọi xuống renderer: phần dựng khung xem
|
||||
chặn GUI thread, nên phải vẽ panel ra trước lúc đó. Nó tự ẩn khi
|
||||
``graph_rendered`` phát — bao trọn cả lượt quét chạy ở luồng nền phía
|
||||
sau, chứ không tắt ngay khi hàm này trả về.
|
||||
"""
|
||||
da_dung_khung = self.renderer.web is not None
|
||||
self._show_busy("structure.scanning" if da_dung_khung else "structure.loading_view")
|
||||
try:
|
||||
self.renderer.auto_scan_and_fit()
|
||||
finally:
|
||||
# Khong co gi de ve (chua chon thu muc) thi graph_rendered khong phat
|
||||
# — panel se treo lai mai mai neu khong tu don o day.
|
||||
if not self.renderer.path_edit.text().strip():
|
||||
self._hide_busy()
|
||||
|
||||
def set_project(self, project_id: str) -> None:
|
||||
"""Khoá phạm vi quét vào một project (chuỗi rỗng là bỏ khoá)."""
|
||||
self.renderer.set_project(project_id)
|
||||
|
||||
def set_workspace_project(self, project_id: str) -> None:
|
||||
"""Áp project theo màn Workspace — bỏ qua nếu project KHÔNG đổi.
|
||||
|
||||
``WorkspaceTab._bind_project`` gọi xuống đây, và nó chạy lại mỗi lần
|
||||
người dùng vào lại màn Workspace (``_goto`` -> ``refresh`` ->
|
||||
``_load_current`` -> ``_bind_project``). Áp vô điều kiện thì bộ chọn
|
||||
project của chính màn GraphRAG bị kéo về giá trị của Workspace: chọn một
|
||||
project ở đây, sang tab khác rồi quay lại là mất.
|
||||
|
||||
Đổi sang project khác ở màn Workspace thì vẫn áp — cùng luật với tab Thư
|
||||
mục (``FolderTab.set_project_root``). Chỉ lần refresh trong CÙNG một
|
||||
project là không được đụng.
|
||||
"""
|
||||
if project_id == self._workspace_project:
|
||||
return
|
||||
self._workspace_project = project_id
|
||||
self.set_project(project_id)
|
||||
|
||||
def prewarm(self) -> None:
|
||||
"""Dựng sẵn khung đồ thị trước khi người dùng bấm vào, để lần mở đầu không giật."""
|
||||
self.renderer.prewarm()
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Project chọn ở tab GraphRAG không được biến mất khi chuyển tab.
|
||||
|
||||
Cùng khuôn với ``test_folder_root_persists.py``: ``WorkspaceTab._bind_project``
|
||||
gọi ``self._structure.set_project(pid)`` vô điều kiện, và nó chạy lại mỗi lần
|
||||
người dùng vào lại màn Workspace (``_goto`` → ``refresh`` → ``_load_current`` →
|
||||
``_bind_project``). Nên bộ chọn project của chính màn GraphRAG bị kéo về giá trị
|
||||
của Workspace mỗi cú chuyển tab.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("PySide6", reason="cần PySide6 để dựng widget thật")
|
||||
|
||||
|
||||
class _Ghi:
|
||||
"""Thay GraphRenderer — chỉ ghi lại nó bị áp project mấy lần, với giá trị gì."""
|
||||
|
||||
def __init__(self):
|
||||
self.lan_ap = []
|
||||
|
||||
def set_project(self, project_id):
|
||||
self.lan_ap.append(project_id)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def view(qapp, monkeypatch):
|
||||
"""StructureGraphView với renderer bị thay — dựng renderer thật kéo theo
|
||||
QtWebEngine, quá đắt cho một khẳng định về luồng điều khiển."""
|
||||
from cowork_local.presentation.graph.structure_graph_view import StructureGraphView
|
||||
|
||||
v = StructureGraphView.__new__(StructureGraphView)
|
||||
v._workspace_project = None
|
||||
v.renderer = _Ghi()
|
||||
return v
|
||||
|
||||
|
||||
def test_lan_dau_van_ap_du_project_la_rong(view):
|
||||
"""``None`` khởi tạo để lần gọi đầu không bị bỏ qua khi pid là chuỗi rỗng."""
|
||||
view.set_workspace_project("")
|
||||
|
||||
assert view.renderer.lan_ap == [""]
|
||||
|
||||
|
||||
def test_refresh_cung_project_khong_ap_lai(view):
|
||||
"""Đây chính là triệu chứng: chuyển tab rồi quay lại là mất lựa chọn."""
|
||||
view.set_workspace_project("p-a")
|
||||
view.set_workspace_project("p-a") # chuyển tab rồi quay lại -> refresh
|
||||
view.set_workspace_project("p-a")
|
||||
|
||||
assert view.renderer.lan_ap == ["p-a"], "áp lại là kéo bộ chọn về, xoá lựa chọn tay"
|
||||
|
||||
|
||||
def test_doi_project_o_workspace_thi_van_ap(view):
|
||||
"""Đổi project thật thì GraphRAG phải theo."""
|
||||
view.set_workspace_project("p-a")
|
||||
view.set_workspace_project("p-b")
|
||||
|
||||
assert view.renderer.lan_ap == ["p-a", "p-b"]
|
||||
|
||||
|
||||
def test_quay_lai_project_cu_thi_ap_lai(view):
|
||||
view.set_workspace_project("p-a")
|
||||
view.set_workspace_project("p-b")
|
||||
view.set_workspace_project("p-a")
|
||||
|
||||
assert view.renderer.lan_ap == ["p-a", "p-b", "p-a"]
|
||||
|
||||
|
||||
def test_bo_khoa_van_di_qua_duoc(view):
|
||||
"""Bỏ chọn project ở Workspace là một thay đổi thật, không phải refresh."""
|
||||
view.set_workspace_project("p-a")
|
||||
view.set_workspace_project("")
|
||||
|
||||
assert view.renderer.lan_ap == ["p-a", ""]
|
||||
|
||||
|
||||
# ---- chỗ gọi phải dùng hàm mới -------------------------------------------
|
||||
|
||||
def test_workspace_tab_goi_set_workspace_project():
|
||||
from pathlib import Path
|
||||
|
||||
src = (Path(__file__).resolve().parents[2] / "ui" / "workspace_tab.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "_structure.set_workspace_project(pid)" in src
|
||||
assert "_structure.set_project(pid)" not in src, (
|
||||
"gọi set_project() vô điều kiện là quay lại đúng lỗi cũ")
|
||||
Reference in New Issue
Block a user