fix: navigatie
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -19,6 +21,216 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
return route
|
||||
self.fail("Expected /ui mount to be registered")
|
||||
|
||||
def _extract_js_function(self, source: str, name: str) -> str:
|
||||
marker = f"function {name}("
|
||||
start = source.find(marker)
|
||||
if start < 0:
|
||||
self.fail(f"Expected function {name} in app.js")
|
||||
brace_start = source.find("{", start)
|
||||
if brace_start < 0:
|
||||
self.fail(f"Expected opening brace for function {name}")
|
||||
depth = 0
|
||||
for index in range(brace_start, len(source)):
|
||||
char = source[index]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[start : index + 1]
|
||||
self.fail(f"Expected closing brace for function {name}")
|
||||
|
||||
def _run_app_js_behavior_check(self, app_js: str) -> None:
|
||||
functions = "\n\n".join(
|
||||
[
|
||||
self._extract_js_function(app_js, "paneState"),
|
||||
self._extract_js_function(app_js, "currentParentPath"),
|
||||
self._extract_js_function(app_js, "baseName"),
|
||||
self._extract_js_function(app_js, "prepareParentReturnRestore"),
|
||||
self._extract_js_function(app_js, "navigateToParent"),
|
||||
self._extract_js_function(app_js, "restoreParentReturnFocus"),
|
||||
self._extract_js_function(app_js, "handleKeyboardShortcuts"),
|
||||
]
|
||||
)
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
const assert = (condition, message) => {{
|
||||
if (!condition) {{
|
||||
throw new Error(message);
|
||||
}}
|
||||
}};
|
||||
|
||||
let state = {{
|
||||
activePane: "left",
|
||||
panes: {{
|
||||
left: {{
|
||||
currentPath: "storage1/parent/submap",
|
||||
showHidden: false,
|
||||
selectedItem: null,
|
||||
selectedItems: [],
|
||||
visibleItems: [],
|
||||
currentRowIndex: -1,
|
||||
selectionAnchorIndex: null,
|
||||
pendingSelectionPath: null,
|
||||
returnFocusName: null,
|
||||
}},
|
||||
right: {{
|
||||
currentPath: "/Volumes",
|
||||
showHidden: false,
|
||||
selectedItem: null,
|
||||
selectedItems: [],
|
||||
visibleItems: [],
|
||||
currentRowIndex: -1,
|
||||
selectionAnchorIndex: null,
|
||||
pendingSelectionPath: null,
|
||||
returnFocusName: null,
|
||||
}},
|
||||
}},
|
||||
}};
|
||||
|
||||
const navigationCalls = [];
|
||||
const renderCalls = [];
|
||||
|
||||
function navigateTo(pane, path) {{
|
||||
navigationCalls.push({{ pane, path }});
|
||||
}}
|
||||
|
||||
function renderPaneItems(pane) {{
|
||||
renderCalls.push({{ pane, currentRowIndex: paneState(pane).currentRowIndex }});
|
||||
const model = paneState(pane);
|
||||
if (!Array.isArray(model.visibleItems) || model.visibleItems.length === 0) {{
|
||||
model.currentRowIndex = -1;
|
||||
return;
|
||||
}}
|
||||
if (model.currentRowIndex < 0 || model.currentRowIndex >= model.visibleItems.length) {{
|
||||
model.currentRowIndex = 0;
|
||||
}}
|
||||
}}
|
||||
|
||||
function isContextMenuOpen() {{ return false; }}
|
||||
function uploadElements() {{ return {{ menuPopup: {{ classList: {{ contains() {{ return true; }} }} }} }}; }}
|
||||
function isFeedbackModalOpen() {{ return false; }}
|
||||
function closeFeedbackModal() {{}}
|
||||
function isDownloadModalOpen() {{ return false; }}
|
||||
function closeDownloadModal() {{}}
|
||||
function isInfoOpen() {{ return false; }}
|
||||
function closeInfo() {{}}
|
||||
function isSearchOpen() {{ return false; }}
|
||||
function closeSearch() {{}}
|
||||
function submitSearch() {{}}
|
||||
function isRenamePopupOpen() {{ return false; }}
|
||||
function closeRenamePopup() {{}}
|
||||
function submitRenamePopup() {{}}
|
||||
function isSettingsOpen() {{ return false; }}
|
||||
function closeSettings() {{}}
|
||||
function isBatchMovePopupOpen() {{ return false; }}
|
||||
function closeBatchMovePopup() {{}}
|
||||
function submitBatchMovePopup() {{}}
|
||||
function isDeleteConfirmModalOpen() {{ return false; }}
|
||||
function closeDeleteConfirmModal() {{}}
|
||||
function submitDeleteConfirmModal() {{}}
|
||||
function isUploadConflictModalOpen() {{ return false; }}
|
||||
function resolveUploadConflict() {{}}
|
||||
function isMovePopupOpen() {{ return false; }}
|
||||
function closeMovePopup() {{}}
|
||||
function submitMovePopup() {{}}
|
||||
function isEditorOpen() {{ return false; }}
|
||||
function attemptCloseEditor() {{}}
|
||||
function isImageOpen() {{ return false; }}
|
||||
function closeImageViewer() {{}}
|
||||
function isVideoOpen() {{ return false; }}
|
||||
function closeVideoViewer() {{}}
|
||||
function isPdfOpen() {{ return false; }}
|
||||
function closePdfViewer() {{}}
|
||||
function isViewerOpen() {{ return false; }}
|
||||
function closeViewer() {{}}
|
||||
function isWildcardPopupOpen() {{ return false; }}
|
||||
function shouldHandleShortcut() {{ return true; }}
|
||||
function openInfo() {{}}
|
||||
function openSearch() {{}}
|
||||
function actionShortcutHandled() {{ return false; }}
|
||||
function openWildcardPopup() {{}}
|
||||
function jumpCurrentRow() {{}}
|
||||
function moveCurrentRow() {{}}
|
||||
function extendSelectionByRow() {{}}
|
||||
function otherPane(pane) {{ return pane === "left" ? "right" : "left"; }}
|
||||
function setActivePane(pane) {{ state.activePane = pane; }}
|
||||
function openCurrentDirectory() {{}}
|
||||
function toggleCurrentSelection() {{}}
|
||||
function clearSelectionForActivePane() {{}}
|
||||
|
||||
{functions}
|
||||
|
||||
let prevented = false;
|
||||
handleKeyboardShortcuts({{
|
||||
key: "Backspace",
|
||||
code: "Backspace",
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
target: null,
|
||||
preventDefault() {{ prevented = true; }},
|
||||
}});
|
||||
assert(prevented, "Backspace should prevent default");
|
||||
assert(navigationCalls.length === 1, "Backspace should trigger parent navigation");
|
||||
assert(navigationCalls[0].path === "storage1/parent", "Backspace should navigate to parent path");
|
||||
assert(paneState("left").returnFocusName === "submap", "Backspace should prepare return focus by child name");
|
||||
assert(paneState("left").pendingSelectionPath === null, "Backspace parent return must not set pending selection path");
|
||||
assert(paneState("left").selectedItems.length === 0, "Backspace parent return must not select items");
|
||||
|
||||
state.panes.left.currentPath = "storage1/parent/submap";
|
||||
state.panes.left.returnFocusName = null;
|
||||
state.panes.left.pendingSelectionPath = null;
|
||||
navigationCalls.length = 0;
|
||||
navigateToParent("left");
|
||||
assert(navigationCalls.length === 1, "Mouse parent-up should trigger parent navigation");
|
||||
assert(navigationCalls[0].path === "storage1/parent", "Mouse parent-up should navigate to parent path");
|
||||
assert(paneState("left").returnFocusName === "submap", "Mouse parent-up should use same restore flow");
|
||||
assert(paneState("left").pendingSelectionPath === null, "Mouse parent-up must not set pending selection path");
|
||||
|
||||
state.panes.left.visibleItems = [
|
||||
{{ path: "storage1/parent", name: "..", kind: "directory", isParent: true }},
|
||||
{{ path: "storage1/parent/submap", name: "submap", kind: "directory" }},
|
||||
{{ path: "storage1/parent/other", name: "other", kind: "directory" }},
|
||||
];
|
||||
state.panes.left.currentRowIndex = 0;
|
||||
state.panes.left.selectedItems = [];
|
||||
state.panes.left.selectedItem = null;
|
||||
state.panes.left.selectionAnchorIndex = null;
|
||||
state.panes.left.returnFocusName = "submap";
|
||||
renderCalls.length = 0;
|
||||
restoreParentReturnFocus("left", state.panes.left.visibleItems);
|
||||
assert(state.panes.left.currentRowIndex === 1, "Restore should focus the child entry in parent");
|
||||
assert(state.panes.left.selectedItems.length === 0, "Restore must not add selection");
|
||||
assert(state.panes.left.selectedItem === null, "Restore must not set selectedItem");
|
||||
assert(state.panes.left.selectionAnchorIndex === null, "Restore must not set selection anchor");
|
||||
assert(state.panes.left.returnFocusName === null, "Restore state should be cleared after success");
|
||||
assert(renderCalls.length >= 1, "Restore should re-render focused row");
|
||||
|
||||
state.panes.left.currentRowIndex = 0;
|
||||
state.panes.left.selectedItems = [];
|
||||
state.panes.left.selectedItem = null;
|
||||
state.panes.left.selectionAnchorIndex = null;
|
||||
state.panes.left.returnFocusName = "missing";
|
||||
renderCalls.length = 0;
|
||||
restoreParentReturnFocus("left", state.panes.left.visibleItems);
|
||||
assert(state.panes.left.currentRowIndex === 0, "Fallback should keep existing row when match is missing");
|
||||
assert(state.panes.left.selectedItems.length === 0, "Fallback must not create selection");
|
||||
assert(state.panes.left.selectedItem === null, "Fallback must not set selectedItem");
|
||||
assert(state.panes.left.selectionAnchorIndex === null, "Fallback must not set selection anchor");
|
||||
assert(state.panes.left.returnFocusName === null, "Restore state should be cleared after failed match");
|
||||
"""
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "-e", script],
|
||||
cwd="/workspace/webmanager-mvp",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr or result.stdout)
|
||||
|
||||
def test_ui_mount_and_index_contains_expected_panels(self) -> None:
|
||||
mount = self._ui_mount()
|
||||
self.assertIsInstance(mount.app, StaticFiles)
|
||||
@@ -228,6 +440,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn("width: min(1180px, calc(100vw - 20px));", base_css)
|
||||
app_js = (static_root / "app.js").read_text(encoding="utf-8")
|
||||
self.assertIn('currentPath: "/Volumes"', app_js)
|
||||
self.assertIn('returnFocusName: null,', app_js)
|
||||
self.assertIn('selectedTheme: "default"', app_js)
|
||||
self.assertIn('selectedColorMode: "dark"', app_js)
|
||||
self.assertIn('const VALID_THEME_FAMILIES = [', app_js)
|
||||
@@ -404,6 +617,13 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
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('if (event.key === "Backspace") {', app_js)
|
||||
self.assertIn('navigateToParent(state.activePane);', app_js)
|
||||
self.assertIn('function navigateToParent(pane) {', app_js)
|
||||
self.assertIn('prepareParentReturnRestore(pane, childPath);', app_js)
|
||||
self.assertIn('function prepareParentReturnRestore(pane, childPath) {', app_js)
|
||||
self.assertIn('model.returnFocusName = baseName(childPath);', app_js)
|
||||
self.assertNotIn('model.pendingSelectionPath = childPath;', app_js)
|
||||
self.assertIn('function collectDeleteRecursivePaths(selectedItems)', app_js)
|
||||
self.assertIn('const confirmed = await openConfirmModal({', app_js)
|
||||
self.assertIn('recursivePaths.has(item.path)', app_js)
|
||||
@@ -430,6 +650,15 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
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('renderPaneItems(pane);', app_js)
|
||||
self.assertIn('restoreParentReturnFocus(pane, visibleItems);', app_js)
|
||||
self.assertIn('function restoreParentReturnFocus(pane, visibleItems) {', app_js)
|
||||
self.assertIn('const returningFromChildName = model.returnFocusName;', app_js)
|
||||
self.assertIn('model.returnFocusName = null;', app_js)
|
||||
self.assertIn('const returnIndex = visibleItems.findIndex((item) => !item.isParent && item.name === returningFromChildName);', app_js)
|
||||
self.assertIn('if (returnIndex < 0) {', app_js)
|
||||
self.assertNotIn('setSingleSelectionAtIndex(pane, selectedEntryFromItem(returnItem), returnIndex);', app_js)
|
||||
self.assertIn('renderPaneItems(pane);', app_js)
|
||||
self.assertIn('settings.interfaceTab.onclick = () => {', app_js)
|
||||
self.assertIn('setSettingsTab("interface");', app_js)
|
||||
self.assertIn('settings.downloadsTab.onclick = () => {', app_js)
|
||||
@@ -440,6 +669,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
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)
|
||||
@@ -576,6 +806,11 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertEqual(base_css_url, "/ui/base.css")
|
||||
self.assertEqual(theme_default_url, "/ui/theme-default.css")
|
||||
|
||||
def test_parent_return_restore_focuses_without_selecting(self) -> None:
|
||||
mount = self._ui_mount()
|
||||
app_js = (Path(mount.app.directory) / "app.js").read_text(encoding="utf-8")
|
||||
self._run_app_js_behavior_check(app_js)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user