- 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.
214 lines
7.6 KiB
Python
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
|