zotero-kb/tests/test_ui.py
Saberlve cf7e2feb19 feat: Implement project management features including rename and delete functionality
- Added project renaming capability in the workspace with appropriate API endpoints.
- Implemented project deletion functionality, ensuring project directories are removed.
- Updated UI to support project renaming and deletion, including inline editing and confirmation dialogs.
- Enhanced batch generation feature for project items with selection and progress tracking.
- Added tests for project renaming and deletion to ensure functionality and integrity.
2026-04-21 16:55:42 +08:00

214 lines
7.6 KiB
Python

import subprocess
from zotero_kb.api import create_app
from zotero_kb.config import AppConfig
def _get_index_html(tmp_path) -> str:
app = create_app(
AppConfig(
workspace_dir=tmp_path / "workspace",
zotero_data_dir=tmp_path / "zotero",
bridge_file=tmp_path / "bridge.json",
)
)
for route in app.routes:
if getattr(route, "path", None) == "/" and "GET" in getattr(route, "methods", set()):
return route.endpoint()
raise AssertionError("GET / route not found")
def test_index_contains_base_page_forms(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert 'id="create-project-form"' in html
assert 'id="recommend-form"' in html
assert 'id="plan-form"' in html
def test_index_contains_import_window_controls(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert 'id="open-import-modal-button"' in html
assert 'id="import-window-shell"' in html
assert 'id="window-minimize-button"' in html
assert 'id="window-maximize-button"' in html
assert 'id="window-close-button"' in html
assert 'id="window-restore-button"' in html
assert 'id="import-window-minimized-bar"' in html
assert 'id="import-window-project-label"' in html
assert 'id="window-clear-selection-button"' in html
assert 'id="window-import-selected-items-button"' in html
assert 'id="window-collection-status"' in html
assert 'id="window-collection-tree"' in html
assert 'id="window-collection-items"' in html
assert 'class="window-toolbar-actions"' in html
assert 'class="window-action-cluster"' in html
assert 'class="window-control-cluster"' in html
assert 'isImportWindowOpen: false' in html
assert "function openImportWindow()" in html
assert "function closeImportWindow()" in html
assert "function updateImportActionState()" in html
assert 'document.body.classList.toggle("modal-open"' in html
def test_index_window_frame_css(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert ".window-frame" in html
assert ".window-shell" in html
assert ".window-toolbar" in html
assert ".window-body" in html
assert ".window-minimized-bar" in html
assert '"zotero-kb.import-window"' in html
assert "startWindowDrag" in html
assert "minimizeWindow" in html
assert "maximizeWindow" in html
assert "restoreWindow" in html
assert "initWindowFromSession" in html
assert "persistWindowSessionState" in html
def test_index_import_window_has_collection_import_layout(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert 'class="window-pane collections"' in html
assert 'class="window-pane items"' in html
assert 'id="window-collection-status"' in html
assert 'id="window-collection-tree"' in html
assert 'id="window-collection-items"' in html
assert ".window-pane.collections" in html
assert ".window-pane.items" in html
assert ".tree-list" in html
assert ".collection-items-list" in html
def test_index_has_collection_import_api_calls(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert "function loadCollectionTree()" in html
assert "function loadCollectionItems(collectionKey)" in html
assert "/api/zotero/collections/tree" in html
assert "/api/zotero/collections/${collectionKey}/items" in html
assert "/api/projects/" in html
assert "imports/item-keys" in html
assert "function importSelectedItems()" in html
# openImportWindow calls loadCollectionTree
assert "openImportWindow()" in html
assert "await loadCollectionTree()" in html
def test_index_has_responsive_import_window_geometry_helpers(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert "function computeResponsiveWindowRect()" in html
assert "function applyResponsiveWindowRect()" in html
assert "function syncWindowToViewport()" in html
assert 'window.addEventListener("resize"' in html
def test_index_has_responsive_import_window_layout_rules(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert "@media (max-width: 980px)" in html
assert ".window-pane.items" in html
assert ".window-body {" in html
assert "grid-template-columns: 1fr;" in html
assert "border-bottom: 1px solid var(--line);" in html
def test_index_has_project_item_list_and_inline_detail_hooks(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert 'id="project-item-list"' in html
assert "expandedProjectItemKey" in html
assert "function renderProjectItems(items)" in html
assert "function toggleProjectItemDetail(itemKey)" in html
assert ".project-item-detail" in html
def test_index_collection_rows_use_readable_unselected_text(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert ".project-item strong" in html
assert ".project-item .meta" in html
assert ".collection-row-toggle" in html
def test_index_project_item_active_state_uses_high_contrast_text(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert ".project-item.active {" in html
assert "background: var(--accent-strong);" in html
assert "color: #f7f2e8;" in html
assert ".project-item.active strong," in html
assert ".project-item.active .meta {" in html
assert "color: inherit;" in html
def test_index_has_project_rename_delete_controls(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert "editingProjectId" in html
assert "confirmingDeleteProjectId" in html
assert "function startProjectRename(projectId)" in html
assert "function cancelProjectRename()" in html
assert "function saveProjectRename(projectId)" in html
assert "function confirmProjectDelete(projectId)" in html
assert "function deleteProject(projectId)" in html
assert 'method: "DELETE"' in html
assert 'method: "POST"' in html
assert 'api(`/api/projects/${projectId}/delete`' in html
assert 'error.message === "Method Not Allowed"' in html
assert ".project-list-entry" in html
assert ".project-list-main" in html
assert ".project-list-actions" in html
assert ".project-inline-form" in html
assert ".project-delete-confirmation" in html
def test_index_has_project_item_generate_actions_and_batch_window(tmp_path) -> None:
html = _get_index_html(tmp_path)
assert 'id="open-batch-generate-button"' in html
assert 'id="batch-generate-shell"' in html
assert 'id="batch-generate-window"' in html
assert 'id="batch-generate-list"' in html
assert 'id="select-pending-batch-button"' in html
assert 'id="clear-batch-selection-button"' in html
assert 'id="start-batch-generate-button"' in html
assert 'id="batch-generate-progress"' in html
assert "function openBatchGenerateWindow()" in html
assert "function closeBatchGenerateWindow()" in html
assert "function generateProjectItem(itemKey)" in html
assert "function selectAllPendingBatchItems()" in html
assert "function runBatchGenerate()" in html
assert ".project-item-status-pill" in html
assert ".project-item-generate-button" in html
def test_index_inline_script_is_valid_javascript(tmp_path) -> None:
html = _get_index_html(tmp_path)
script_start = html.rfind("<script>")
script_end = html.rfind("</script>")
assert script_start != -1
assert script_end != -1
script_path = tmp_path / "index-inline.js"
script_path.write_text(html[script_start + len("<script>") : script_end], encoding="utf-8")
result = subprocess.run(
["node", "--check", str(script_path)],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr