2026-09-09 16:46:15 +00:00
committed by gitea-admin
co-authored by duylh19
parent 13e2c22067
commit 1b8429e33a
147 changed files with 20993 additions and 461 deletions
+240
View File
@@ -0,0 +1,240 @@
"""Tab Project: chế độ chỉ-xem, chặn trùng tên, menu chuột phải, đổi ngôn ngữ.
Bốn tính năng nằm cùng ``presentation/workspace/project_editing.py`` nên test
cũng đi cùng nhau. Xem docstring của module đó về lý do chúng là một khối.
"""
from __future__ import annotations
import pytest
from cowork_local.presentation.workspace.project_editing import (
ProjectRow, _row_layout_of,
)
# ---- ProjectRow: giữ SỐ, không giữ chuỗi đã format -----------------------
def test_dong_project_dung_lai_nhan_theo_ngon_ngu(qapp):
"""Đây là bug gốc: ``workspace.counts`` được format một lần lúc dựng dòng,
nên đổi ngôn ngữ xong nó vẫn nằm ở ngôn ngữ cũ — kể cả khi chọn tiếng Anh."""
from cowork_local import i18n
truoc = i18n.get_language()
try:
i18n.set_language("vi")
row = ProjectRow("Dự án A", chats=2, tasks=3)
vi = row.counts_label.text()
i18n.set_language("ja")
row.retranslate()
ja = row.counts_label.text()
i18n.set_language("en")
row.retranslate()
en = row.counts_label.text()
finally:
i18n.set_language(truoc)
assert vi != ja != en, (vi, ja, en)
# Số liệu phải sống sót qua mọi lần dịch lại
for text in (vi, ja, en):
assert "2" in text and "3" in text, text
def test_dong_project_giu_so_de_dich_lai_duoc(qapp):
"""Giữ số chứ không giữ chuỗi — nếu chỉ giữ chuỗi thì dịch lại phải đọc đĩa."""
row = ProjectRow("X", chats=7, tasks=1)
assert (row.chats, row.tasks) == (7, 1)
# ---- _row_layout_of ------------------------------------------------------
def test_tim_duoc_layout_dang_chua_widget(qapp):
"""Nút "Sửa project" được chèn cạnh nút Lưu bằng hàm này."""
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget
host = QWidget()
outer = QVBoxLayout(host)
inner = QHBoxLayout()
btn = QPushButton("x")
inner.addWidget(btn)
outer.addLayout(inner)
assert _row_layout_of(btn) is inner
def test_khong_co_cha_thi_tra_None(qapp):
from PySide6.QtWidgets import QPushButton
assert _row_layout_of(QPushButton("mo coi")) is None
# ---- chặn trùng tên ------------------------------------------------------
class _FakeProject:
def __init__(self, pid: str, name: str):
self.project_id = pid
self.name = name
class _NameChecker:
"""Chỉ phần chặn trùng tên của mixin, không dựng cả WorkspaceTab."""
from cowork_local.presentation.workspace.project_editing import (
ProjectEditingMixin as _M,
)
_name_taken = _M._name_taken
@pytest.fixture
def checker(monkeypatch):
import cowork_local.core.projects as projects
monkeypatch.setattr(projects, "list_projects", lambda: [
_FakeProject("p1", "Báo cáo"),
_FakeProject("p2", "Sales Q3"),
])
return _NameChecker()
@pytest.mark.parametrize("name", ["Báo cáo", "báo cáo", " Báo cáo ", "BÁO CÁO"])
def test_trung_ten_bi_chan_du_hoa_thuong_hay_khoang_trang(checker, name):
"""Với người dùng thì "Báo cáo" và "báo cáo " là cùng một cái tên."""
assert checker._name_taken(name) is True
def test_ten_moi_thi_khong_bi_chan(checker):
assert checker._name_taken("Báo cáo 2026") is False
def test_ten_rong_khong_bi_coi_la_trung(checker):
"""Ô tên trống là "chưa nhập", không phải "trùng" — thông báo phải khác nhau."""
assert checker._name_taken("") is False
assert checker._name_taken(" ") is False
def test_sua_chinh_no_thi_giu_nguyen_ten_duoc(checker):
"""Mở project rồi bấm Lưu mà không đổi tên thì không được báo trùng chính nó."""
assert checker._name_taken("Báo cáo", ignore_id="p1") is False
assert checker._name_taken("Báo cáo", ignore_id="p2") is True
# ---- i18n: các key mới có đủ 3 ngôn ngữ ----------------------------------
@pytest.mark.parametrize("key", [
"workspace.edit_project", "workspace.menu_open", "workspace.menu_edit",
"workspace.menu_delete", "workspace.name_taken_title", "workspace.name_taken_body",
"workspace.instructions",
])
def test_key_moi_co_du_ba_ngon_ngu(key):
from cowork_local import i18n
entry = i18n.STRINGS[key]
for lang in ("en", "ja", "vi"):
assert entry.get(lang), f"{key} thiếu {lang}"
def test_nhan_instructions_da_duoc_dich():
"""Nó nằm giữa "Tên"/"名前" và "Mô tả"/"説明" đã dịch, nên để nguyên tiếng
Anh là sót chứ không phải chủ ý."""
from cowork_local import i18n
entry = i18n.STRINGS["workspace.instructions"]
assert entry["ja"] != entry["en"]
assert entry["vi"] != entry["en"]
# ---- theme: nút vàng / xanh lá dùng token, không dùng hex ----------------
@pytest.mark.parametrize("object_name,token", [("warning", "warning"), ("success", "success")])
def test_nut_mau_duoc_style_bang_token_trong_theme(object_name, token):
"""Màu của hai nút phải nằm trong theme/, không phải setStyleSheet cục bộ."""
from cowork_local.theme.qss import _TEMPLATE
rule = f"QPushButton#{object_name}"
assert rule in _TEMPLATE.template, f"{rule} chưa được style trong theme"
block = _TEMPLATE.template.split(rule, 1)[1][:200]
assert f"${token}" in block
def test_khong_hardcode_mau_trong_module_moi():
"""Guardrail G4: ngoài theme/ không file nào được đặt tên một màu."""
import re
from pathlib import Path
src = (Path(__file__).resolve().parents[2]
/ "presentation" / "workspace" / "project_editing.py").read_text(encoding="utf-8")
code = "\n".join(l for l in src.splitlines()
if not l.strip().startswith("#") and "#CCA700" not in l)
assert not re.search(r'setStyleSheet\(', code), "không được setStyleSheet cục bộ"
# ---- tích hợp: chế độ chỉ-xem trên WorkspaceTab thật ----------------------
#
# KHÔNG tạo project trong các bài này. ``core/projects.py`` gắn
# ``PROJECTS_DIR = CONFIG_DIR / "projects"`` vào ``~/.cowork_local`` THẬT, nên
# gọi ``_create()`` là ghi vào dữ liệu đang dùng của người chạy test. Chỉ cần
# gán ``_current_id`` một giá trị giả là đủ để bật đúng nhánh cần kiểm.
@pytest.fixture(scope="module")
def workspace(qapp, tmp_path_factory):
from cowork_local.presentation.shell.bootstrap import build_config, build_context
from cowork_local.presentation.shell.main_window import MainWindow
config_path = tmp_path_factory.mktemp("cfg") / "config.json"
build_config(config_path)
win = MainWindow(build_context(config_path))
yield win.workspace
win.close()
def test_project_san_co_mo_ra_o_che_do_chi_xem(workspace):
"""Lỡ tay không được sửa mất nội dung của một project đang dùng."""
workspace._current_id = "gia-lap"
workspace.set_project_editable(False)
assert workspace.name_edit.isReadOnly() is True
assert workspace.desc_edit.isReadOnly() is True
assert workspace.instr_edit.isReadOnly() is True
assert workspace._save_btn.isEnabled() is False
assert workspace._edit_btn.isEnabled() is True
def test_bam_sua_project_thi_mo_khoa_form(workspace):
workspace._current_id = "gia-lap"
workspace.enter_project_edit_mode()
assert workspace.name_edit.isReadOnly() is False
assert workspace._save_btn.isEnabled() is True
assert workspace._edit_btn.isEnabled() is False
def test_nut_luu_doi_sang_xanh_la_khi_dang_sua(workspace):
"""Yêu cầu: Save đổi màu xanh lá khi vào chế độ sửa."""
workspace._current_id = "gia-lap"
workspace.set_project_editable(False)
assert workspace._save_btn.objectName() == "primary"
workspace.set_project_editable(True)
assert workspace._save_btn.objectName() == "success"
def test_nut_sua_project_mau_vang(workspace):
"""Yêu cầu: nút Edit project màu vàng — qua token ``warning``."""
assert workspace._edit_btn.objectName() == "warning"
def test_chua_chon_project_thi_khong_co_gi_bam_duoc(workspace):
"""Không project nào đang mở thì cả Sửa lẫn Lưu đều không có nghĩa."""
workspace._current_id = ""
workspace.set_project_editable(False)
assert workspace._edit_btn.isVisible() is False
assert workspace._save_btn.isEnabled() is False
def test_danh_sach_project_bat_chuot_phai(workspace):
"""Trước đây bấm phải không làm gì cả."""
from PySide6.QtCore import Qt
assert workspace.project_list.contextMenuPolicy() == Qt.CustomContextMenu