Files
webmanager-mvp/webui/backend/tests/golden/test_ui_smoke_golden.py
T

407 lines
24 KiB
Python

from __future__ import annotations
import sys
import unittest
from pathlib import Path
from fastapi.staticfiles import StaticFiles
from starlette.routing import Mount
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
from backend.app.main import app
class UiSmokeGoldenTest(unittest.TestCase):
def _ui_mount(self) -> Mount:
for route in app.routes:
if isinstance(route, Mount) and route.path == "/ui":
return route
self.fail("Expected /ui mount to be registered")
def test_ui_mount_and_index_contains_expected_panels(self) -> None:
mount = self._ui_mount()
self.assertIsInstance(mount.app, StaticFiles)
index_path = Path(mount.app.directory) / "index.html"
self.assertTrue(index_path.exists())
body = index_path.read_text(encoding="utf-8")
self.assertIn('/ui/base.css', body)
self.assertIn('/ui/theme-default.css', body)
self.assertIn('/ui/theme-macos-soft.css', body)
self.assertIn('/ui/theme-midnight.css', body)
self.assertIn('/ui/theme-graphite.css', body)
self.assertIn('/ui/theme-windows11.css', body)
self.assertIn('/ui/theme-commander-electric.css', body)
self.assertIn('/ui/theme-nord-arctic.css', body)
self.assertIn('/ui/theme-catppuccin-soft.css', body)
self.assertIn('/ui/theme-fluent-neon.css', body)
self.assertIn('id="workspace"', body)
self.assertIn('id="footer-bar"', body)
self.assertIn('id="title-zone-actions"', body)
self.assertIn('id="status"', body)
self.assertIn('id="theme-toggle"', body)
self.assertIn('id="theme-toggle-icon"', body)
self.assertIn('id="left-pane"', body)
self.assertIn('id="right-pane"', body)
self.assertIn('id="left-items"', body)
self.assertIn('id="right-items"', body)
self.assertIn('id="left-focus-line"', body)
self.assertIn('id="right-focus-line"', body)
self.assertIn('id="function-bar"', body)
self.assertIn('id="upload-btn"', body)
self.assertIn('id="upload-menu-toggle"', body)
self.assertIn('id="upload-menu-popup"', body)
self.assertIn('id="upload-folder-btn"', body)
self.assertIn('id="upload-input"', body)
self.assertIn('id="upload-menu"', body)
self.assertIn('id="upload-menu-toggle"', body)
self.assertIn('id="upload-menu-popup"', body)
self.assertIn('id="upload-folder-btn"', body)
self.assertIn('id="upload-modal"', body)
self.assertIn('id="upload-modal-target"', body)
self.assertIn('id="upload-modal-current-file"', body)
self.assertIn('id="upload-modal-progress-bar"', body)
self.assertIn('id="upload-modal-count"', body)
self.assertIn('id="upload-modal-status"', body)
self.assertIn('id="upload-modal-cancel-btn"', body)
self.assertIn('id="feedback-modal"', body)
self.assertIn('id="feedback-message"', body)
self.assertIn('id="feedback-close-btn"', body)
self.assertIn('id="context-menu"', body)
self.assertIn('id="context-menu-scope"', body)
self.assertIn('id="context-menu-target"', body)
self.assertIn('id="context-menu-rename-btn"', body)
self.assertIn('id="context-menu-copy-btn"', body)
self.assertIn('id="context-menu-move-btn"', body)
self.assertIn('id="context-menu-delete-btn"', body)
self.assertIn('id="settings-btn"', body)
self.assertIn('id="rename-btn"', body)
self.assertIn('id="view-btn"', body)
self.assertIn('id="edit-btn"', body)
self.assertIn("F1", body)
self.assertIn("F2", body)
self.assertIn("F3", body)
self.assertIn("F4", body)
self.assertIn("F5", body)
self.assertIn("F6", body)
self.assertIn("F7", body)
self.assertIn("F8", body)
self.assertIn('id="viewer-modal"', body)
self.assertIn('id="video-modal"', body)
self.assertIn('id="pdf-modal"', body)
self.assertIn('id="image-modal"', body)
self.assertIn('id="image-viewer-img"', body)
self.assertIn('id="image-zoom-in-btn"', body)
self.assertIn('id="image-zoom-out-btn"', body)
self.assertIn('id="image-reset-btn"', body)
self.assertIn('id="pdf-frame"', body)
self.assertIn('id="pdf-close-btn"', body)
self.assertIn('id="video-player"', body)
self.assertIn('id="video-close-btn"', body)
self.assertIn('id="settings-modal"', body)
self.assertIn('id="search-modal"', body)
self.assertIn('id="search-input"', body)
self.assertIn('id="search-results"', body)
self.assertIn('id="info-modal"', body)
self.assertIn('id="rename-popup"', body)
self.assertIn('id="rename-input"', body)
self.assertIn('id="rename-apply-btn"', body)
self.assertIn('id="settings-general-tab"', body)
self.assertIn('id="settings-interface-tab"', body)
self.assertIn('id="settings-logs-tab"', body)
self.assertIn('id="settings-show-thumbnails"', body)
self.assertIn("Show thumbnails", body)
self.assertIn('id="settings-selected-theme"', body)
self.assertIn("Theme", body)
self.assertIn('value="default"', body)
self.assertIn('value="macos-soft"', body)
self.assertIn('value="midnight"', body)
self.assertIn('value="graphite"', body)
self.assertIn('value="windows11"', body)
self.assertIn('value="commander-electric"', body)
self.assertIn('value="nord-arctic"', body)
self.assertIn('value="catppuccin-soft"', body)
self.assertIn('value="fluent-neon"', body)
self.assertNotIn('id="settings-selected-color-mode"', body)
self.assertIn('id="settings-startup-path-left"', body)
self.assertIn('id="settings-startup-path-right"', body)
self.assertIn("Preferred startup path (left)", body)
self.assertIn("Preferred startup path (right)", body)
self.assertIn('id="settings-general-save-btn"', body)
self.assertIn('id="settings-interface-save-btn"', body)
self.assertIn('id="settings-logs-list"', body)
self.assertIn('id="viewer-content"', body)
self.assertIn('id="editor-modal"', body)
self.assertIn('id="editor-host"', body)
self.assertIn('id="editor-save-btn"', body)
self.assertIn('id="editor-cancel-btn"', body)
self.assertIn('id="move-popup"', body)
self.assertIn('id="move-input"', body)
self.assertIn(">Move</h3>", body)
self.assertIn(">Target path</label>", body)
self.assertIn('id="batch-move-popup"', body)
self.assertIn('id="batch-move-apply-btn"', body)
self.assertIn('id="delete-confirm-modal"', body)
self.assertIn('id="delete-confirm-message"', body)
self.assertIn('id="delete-confirm-apply-btn"', body)
self.assertIn('id="delete-confirm-cancel-btn"', body)
self.assertIn("Delete folder and contents?", body)
self.assertIn('id="mkdir-btn"', body)
self.assertIn('id="copy-btn"', body)
self.assertIn('id="move-btn"', body)
self.assertIn('id="rename-btn"', body)
self.assertIn('id="delete-btn"', body)
self.assertIn('id="left-breadcrumbs"', body)
self.assertIn('id="right-breadcrumbs"', body)
self.assertIn('id="wildcard-popup"', body)
self.assertIn('id="wildcard-pattern-input"', body)
self.assertNotIn('id="search-btn"', body)
self.assertNotIn('id="info-btn"', body)
self.assertNotIn('id="bookmarks-panel"', body)
self.assertNotIn('id="tasks-panel"', body)
ordered_ids = [
'id="upload-btn"',
'id="settings-btn"',
'id="rename-btn"',
'id="view-btn"',
'id="edit-btn"',
'id="copy-btn"',
'id="move-btn"',
'id="mkdir-btn"',
'id="delete-btn"',
]
positions = [body.index(marker) for marker in ordered_ids]
self.assertEqual(positions, sorted(positions))
def test_ui_static_assets_are_present_and_mapped(self) -> None:
mount = self._ui_mount()
static_root = Path(mount.app.directory)
self.assertTrue((static_root / "app.js").exists())
self.assertTrue((static_root / "base.css").exists())
self.assertTrue((static_root / "theme-default.css").exists())
self.assertTrue((static_root / "theme-macos-soft.css").exists())
self.assertTrue((static_root / "theme-midnight.css").exists())
self.assertTrue((static_root / "theme-graphite.css").exists())
self.assertTrue((static_root / "theme-windows11.css").exists())
self.assertTrue((static_root / "theme-commander-electric.css").exists())
self.assertTrue((static_root / "theme-nord-arctic.css").exists())
self.assertTrue((static_root / "theme-catppuccin-soft.css").exists())
self.assertTrue((static_root / "theme-fluent-neon.css").exists())
app_js = (static_root / "app.js").read_text(encoding="utf-8")
self.assertIn('currentPath: "/Volumes"', app_js)
self.assertIn('selectedTheme: "default"', app_js)
self.assertIn('selectedColorMode: "dark"', app_js)
self.assertIn('const VALID_THEME_FAMILIES = [', app_js)
self.assertIn('"commander-electric"', app_js)
self.assertIn('"nord-arctic"', app_js)
self.assertIn('"catppuccin-soft"', app_js)
self.assertIn('"fluent-neon"', app_js)
self.assertIn('document.documentElement.dataset.themeFamily', app_js)
self.assertIn('document.documentElement.dataset.colorMode', app_js)
self.assertIn('function effectiveThemeKey(theme, colorMode)', app_js)
self.assertIn("document.documentElement.dataset.theme", app_js)
self.assertIn('document.getElementById("theme-toggle").onclick = toggleTheme;', app_js)
self.assertIn('document.getElementById("upload-btn").onclick = openUploadPicker;', app_js)
self.assertIn('function feedbackElements()', app_js)
self.assertIn('function openFeedbackModal(message)', app_js)
self.assertIn('function closeFeedbackModal()', app_js)
self.assertIn('function contextMenuElements()', app_js)
self.assertIn('function openContextMenu(pane, entry, event)', app_js)
self.assertIn('function closeContextMenu()', app_js)
self.assertIn('function applyContextMenuSelection()', app_js)
self.assertIn('function startContextMenuRename()', app_js)
self.assertIn('function startContextMenuCopy()', app_js)
self.assertIn('function startContextMenuMove()', app_js)
self.assertIn('function startContextMenuDelete()', app_js)
self.assertIn('selectedPathsSet.has(entry.path)', app_js)
self.assertIn('entry.isParent', app_js)
self.assertIn('row.oncontextmenu = (event) => {', app_js)
self.assertIn('event.target.closest("li[data-row-index]")', app_js)
self.assertIn('if (!row) {', app_js)
self.assertIn('closeContextMenu();', app_js)
self.assertIn('elements.renameButton.classList.toggle("hidden", isMulti);', app_js)
self.assertIn('elements.copyButton.classList.remove("hidden");', app_js)
self.assertIn('elements.copyButton.disabled = items.length === 0;', app_js)
self.assertIn('elements.moveButton.classList.remove("hidden");', app_js)
self.assertIn('openRenamePopup();', app_js)
self.assertIn('startCopySelected();', app_js)
self.assertIn('openF6Flow();', app_js)
self.assertIn('deleteSelected();', app_js)
self.assertIn('document.getElementById("copy-btn").disabled = !hasSelection;', app_js)
self.assertNotIn('Only files are supported for copy', app_js)
self.assertIn('document.getElementById("upload-menu-toggle").onclick = (event) => {', app_js)
self.assertIn('document.getElementById("upload-folder-btn").onclick = openFolderPicker;', app_js)
self.assertIn('throw createApiError(response, data);', app_js)
self.assertIn('function closeUploadMenu()', app_js)
self.assertIn('function toggleUploadMenu()', app_js)
self.assertNotIn('if (event.altKey) {', app_js)
self.assertIn('document.getElementById("settings-btn").onclick = () => openSettings("general");', app_js)
self.assertIn('function collectDeleteRecursivePaths(selectedItems)', app_js)
self.assertIn('openDeleteConfirmModal(pane, selectedItems, recursivePaths);', app_js)
self.assertIn('recursivePaths.has(item.path)', app_js)
self.assertIn('Delete selected items and folder contents?', app_js)
self.assertIn('async function loadSettings()', app_js)
self.assertIn('await loadSettings();', app_js)
self.assertIn('settings.showThumbnailsInput.onchange = handleShowThumbnailsChange;', app_js)
self.assertIn('settings.generalSaveButton.onclick = handlePreferredStartupPathSave;', app_js)
self.assertIn('settings.interfaceSaveButton.onclick = handleInterfaceSave;', app_js)
self.assertIn('preferredStartupPathLeft', app_js)
self.assertIn('preferredStartupPathRight', app_js)
self.assertIn('selected_theme', app_js)
self.assertIn('selected_color_mode', app_js)
self.assertNotIn("localStorage", app_js)
self.assertNotIn("THEME_STORAGE_KEY", app_js)
self.assertIn('preferred_startup_path_left', app_js)
self.assertIn('preferred_startup_path_right', app_js)
self.assertIn('paneState("left").currentPath = settingsState.preferredStartupPathLeft || "/Volumes";', app_js)
self.assertIn('paneState("right").currentPath = settingsState.preferredStartupPathRight || "/Volumes";', app_js)
self.assertIn('applyTheme(settingsState.selectedTheme, settingsState.selectedColorMode);', app_js)
self.assertIn('settings.interfaceTab.onclick = () => setSettingsTab("interface");', app_js)
self.assertIn('"/api/settings"', app_js)
self.assertIn('function uploadElements()', app_js)
self.assertIn('function openUploadPicker()', app_js)
self.assertIn('function ensureFolderUploadPicker()', app_js)
self.assertIn('function openFolderPicker()', app_js)
self.assertIn('function uploadModalElements()', app_js)
self.assertIn('function setUploadModalVisible(', app_js)
self.assertIn('function updateUploadModalDisplay(', app_js)
self.assertIn('function buildFolderUploadPlan(files, targetPath)', app_js)
self.assertIn('function folderDirectoryPaths(plan)', app_js)
self.assertIn('async function ensureFolderDirectoryExists(path)', app_js)
self.assertIn('async function executeFolderUploadPlan(plan)', app_js)
self.assertIn('async function handleFolderSelection(event)', app_js)
self.assertIn('function deleteConfirmElements()', app_js)
self.assertIn('function openDeleteConfirmModal(pane, items, recursivePaths)', app_js)
self.assertIn('async function executeDeleteItems(pane, items, recursivePaths)', app_js)
self.assertIn('async function submitDeleteConfirmModal()', app_js)
self.assertIn('recursive: recursivePaths.has(item.path)', app_js)
self.assertIn('input.setAttribute("webkitdirectory", "")', app_js)
self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js)
self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', app_js)
self.assertIn('Folder upload: preparing', app_js)
self.assertIn('Folder upload: ${uploadState.successfulCount} uploaded, ${uploadState.skippedCount} skipped', app_js)
self.assertIn('async function handleUploadSelection(event)', app_js)
self.assertIn('uploadElements().input.onchange = handleUploadSelection;', app_js)
self.assertIn('"/api/files/upload"', app_js)
self.assertIn('function ensureUploadConflictModal()', app_js)
self.assertIn('function promptUploadConflict(', app_js)
self.assertIn('formData.append("overwrite", overwrite ? "true" : "false")', app_js)
self.assertIn('createButton("Overwrite"', app_js)
self.assertIn('createButton("Overwrite all"', app_js)
self.assertIn('createButton("Skip"', app_js)
self.assertIn('createButton("Skip all"', app_js)
self.assertIn('createButton("Cancel"', app_js)
self.assertIn('if (err.code !== "already_exists") {', app_js)
self.assertIn('if (choice === "overwrite_all") {', app_js)
self.assertIn('if (uploadState.skipAll) {', app_js)
self.assertIn('if (choice === "skip_all") {', app_js)
self.assertIn('uploadState.skipAll = true;', app_js)
self.assertIn('Upload to: ${uploadState.targetPath}', app_js)
self.assertIn('Uploading ${total} file', app_js)
self.assertIn('`/api/files/thumbnail?', app_js)
self.assertIn("function iconTypeForEntry(entry)", app_js)
self.assertIn("function mediaIconSvg(type)", app_js)
self.assertIn('const iconType = iconTypeForEntry(entry);', app_js)
self.assertIn('function createMediaSlot(entry)', app_js)
self.assertIn('function createSelectionSlot(pane, entry, index)', app_js)
self.assertIn('entry-select-slot', app_js)
self.assertIn('entry-select-toggle', app_js)
self.assertIn('toggleSelectionAtIndex(pane, selectedEntryFromItem(entry), index);', app_js)
self.assertIn('function openSearch()', app_js)
self.assertIn('async function submitSearch()', app_js)
self.assertIn('async function openInfo()', app_js)
self.assertIn('function imageElements()', app_js)
self.assertIn('function isImageSelection(item)', app_js)
self.assertIn('async function openImageViewer()', app_js)
self.assertIn('if (isImageSelection(item)) {', app_js)
self.assertIn('openImageViewer();', app_js)
self.assertIn('function isImageOpen()', app_js)
self.assertIn('requestAnimationFrame(() => {', app_js)
self.assertIn('if (!fitImageToViewport()) {', app_js)
self.assertIn('if (!viewport.clientWidth || !viewport.clientHeight) {', app_js)
self.assertIn("`/api/files/image?", app_js)
self.assertIn('if (isImageSelection(selected)) {', app_js)
self.assertIn('document.getElementById("info-modal")', app_js)
self.assertIn("`/api/files/info?", app_js)
self.assertIn('document.getElementById("search-input")', app_js)
self.assertIn("`/api/search?", app_js)
self.assertIn('event.key.toLowerCase() === "f"', app_js)
self.assertIn('(event.metaKey || event.ctrlKey)', app_js)
self.assertIn('const isInfoShortcut = event.key === "Enter"', app_js)
self.assertIn('if (event.key === "F1") {', app_js)
self.assertIn('if (event.key === "F2") {', app_js)
self.assertIn('function openSettings(tab = "general")', app_js)
self.assertIn('function openRenamePopup()', app_js)
self.assertIn('document.getElementById("rename-btn").onclick = openRenamePopup;', app_js)
self.assertIn('return triggerActionButton("rename-btn");', app_js)
self.assertIn('".py"', app_js)
self.assertIn('function openVideoViewer()', app_js)
self.assertIn('function openPdfViewer()', app_js)
self.assertIn('async function loadMonacoModule()', app_js)
self.assertIn('async function ensureMonacoEditor(path, content)', app_js)
self.assertIn('function disposeMonacoEditor()', app_js)
self.assertIn('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm', app_js)
self.assertIn('document.getElementById("pdf-modal")', app_js)
self.assertIn("`/api/files/pdf?", app_js)
self.assertIn('if (isPdfSelection(selected)) {', app_js)
self.assertIn('video.player.src = streamUrl;', app_js)
self.assertIn('document.getElementById("video-close-btn")', app_js)
self.assertIn('row.ondblclick = (ev) => {', app_js)
self.assertIn('function openMovePopup()', app_js)
self.assertIn('document.getElementById("move-btn").onclick = openF6Flow;', app_js)
self.assertIn('await apiRequest("GET", "/api/history")', app_js)
self.assertIn('Cross-root directory move is not supported in v1', app_js)
self.assertIn('Batch directory move is not supported in v1', app_js)
self.assertIn('Batch move requires all selected items to be in the same root', app_js)
self.assertIn('destination_base', app_js)
self.assertIn('sources: selectedItems.map((item) => item.path)', app_js)
self.assertIn("function rootKeyFromPath(path)", app_js)
self.assertIn("function isNestedPath(sourcePath, destinationPath)", app_js)
base_css = (static_root / "base.css").read_text(encoding="utf-8")
default_theme_css = (static_root / "theme-default.css").read_text(encoding="utf-8")
macos_theme_css = (static_root / "theme-macos-soft.css").read_text(encoding="utf-8")
midnight_theme_css = (static_root / "theme-midnight.css").read_text(encoding="utf-8")
graphite_theme_css = (static_root / "theme-graphite.css").read_text(encoding="utf-8")
windows_theme_css = (static_root / "theme-windows11.css").read_text(encoding="utf-8")
commander_theme_css = (static_root / "theme-commander-electric.css").read_text(encoding="utf-8")
nord_theme_css = (static_root / "theme-nord-arctic.css").read_text(encoding="utf-8")
catppuccin_theme_css = (static_root / "theme-catppuccin-soft.css").read_text(encoding="utf-8")
fluent_theme_css = (static_root / "theme-fluent-neon.css").read_text(encoding="utf-8")
self.assertIn('#theme-toggle', base_css)
self.assertIn('.settings-card', base_css)
self.assertIn('.settings-tabs', base_css)
self.assertIn('.entry-media-slot', base_css)
self.assertIn('.entry-media-icon.folder', base_css)
self.assertIn('.entry-media-icon.video', base_css)
self.assertIn('.entry-media-icon.pdf', base_css)
self.assertIn('.entry-media-svg', base_css)
self.assertIn('.entry-media-svg.is-filled', base_css)
self.assertIn('.entry-media-detail', base_css)
self.assertIn('.entry-media-icon.file', base_css)
self.assertIn('.editor-card', base_css)
self.assertIn('.editor-host', base_css)
self.assertNotIn('.select-marker', base_css)
self.assertIn(':root[data-theme-family="default"][data-color-mode="dark"]', default_theme_css)
self.assertIn(':root[data-theme-family="default"][data-color-mode="light"]', default_theme_css)
self.assertIn(':root[data-theme-family="macos-soft"][data-color-mode="dark"]', macos_theme_css)
self.assertIn(':root[data-theme-family="midnight"][data-color-mode="dark"]', midnight_theme_css)
self.assertIn(':root[data-theme-family="graphite"][data-color-mode="dark"]', graphite_theme_css)
self.assertIn(':root[data-theme-family="windows11"][data-color-mode="dark"]', windows_theme_css)
self.assertIn(':root[data-theme-family="commander-electric"][data-color-mode="dark"]', commander_theme_css)
self.assertIn(':root[data-theme-family="nord-arctic"][data-color-mode="dark"]', nord_theme_css)
self.assertIn(':root[data-theme-family="catppuccin-soft"][data-color-mode="dark"]', catppuccin_theme_css)
self.assertIn(':root[data-theme-family="fluent-neon"][data-color-mode="dark"]', fluent_theme_css)
app_js_url = app.url_path_for("ui", path="/app.js")
base_css_url = app.url_path_for("ui", path="/base.css")
theme_default_url = app.url_path_for("ui", path="/theme-default.css")
self.assertEqual(app_js_url, "/ui/app.js")
self.assertEqual(base_css_url, "/ui/base.css")
self.assertEqual(theme_default_url, "/ui/theme-default.css")
if __name__ == "__main__":
unittest.main()