feat: feedback verbetering 04

This commit is contained in:
kodi
2026-03-15 14:52:33 +01:00
parent 3d82699535
commit 61d0c8de41
3 changed files with 154 additions and 42 deletions
@@ -40,6 +40,25 @@ class UiSmokeGoldenTest(unittest.TestCase):
return source[start : index + 1]
self.fail(f"Expected closing brace for function {name}")
def _extract_async_js_function(self, source: str, name: str) -> str:
marker = f"async function {name}("
start = source.find(marker)
if start < 0:
self.fail(f"Expected async function {name} in app.js")
brace_start = source.find("{", start)
if brace_start < 0:
self.fail(f"Expected opening brace for async 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 async function {name}")
def _run_app_js_behavior_check(self, app_js: str) -> None:
functions = "\n\n".join(
[
@@ -592,6 +611,104 @@ class UiSmokeGoldenTest(unittest.TestCase):
)
self.assertEqual(result.returncode, 0, msg=result.stderr or result.stdout)
def _run_operation_start_behavior_check(self, app_js: str) -> None:
functions = "\n\n".join(
[
self._extract_js_function(app_js, "paneState"),
self._extract_js_function(app_js, "otherPane"),
self._extract_js_function(app_js, "defaultDestination"),
self._extract_async_js_function(app_js, "startCopySelected"),
self._extract_async_js_function(app_js, "executeMoveSelection"),
]
)
script = textwrap.dedent(
f"""
const assert = (condition, message) => {{
if (!condition) {{
throw new Error(message);
}}
}};
let state = {{
activePane: "left",
panes: {{
left: {{
currentPath: "storage1/source",
selectedItems: [
{{ path: "storage1/source/a.txt", kind: "file", name: "a.txt" }},
{{ path: "storage1/source/b.txt", kind: "file", name: "b.txt" }},
],
}},
right: {{
currentPath: "storage1/dest",
selectedItems: [],
}},
}},
selectedTaskId: null,
}};
const apiCalls = [];
const statusMessages = [];
const errorCalls = [];
const refreshCalls = [];
const loadCalls = [];
const clearedSelection = [];
async function apiRequest(method, url, body) {{
apiCalls.push({{ method, url, body }});
return {{ task_id: "task-123", status: "queued" }};
}}
function setError() {{}}
function setActionError(action, err) {{ errorCalls.push({{ action, message: err.message }}); }}
function setStatus(message) {{ statusMessages.push(message); }}
async function refreshTasksSnapshot() {{ refreshCalls.push(true); }}
async function loadBrowsePane(pane) {{ loadCalls.push(pane); }}
function setSelectedItem(pane, value) {{ clearedSelection.push({{ pane, value }}); }}
{functions}
(async () => {{
await startCopySelected();
assert(apiCalls.length === 1, "Multi-select copy should issue one request");
assert(apiCalls[0].url === "/api/files/copy", "Copy should use copy endpoint");
assert(Array.isArray(apiCalls[0].body.sources), "Copy should send batch sources");
assert(apiCalls[0].body.sources.length === 2, "Copy batch should include all selected items");
assert(apiCalls[0].body.destination_base === "storage1/dest", "Copy batch should target destination base");
assert(state.selectedTaskId === "task-123", "Copy should store the created task id");
assert(refreshCalls.length === 1, "Copy should refresh task snapshot once");
assert(statusMessages.includes("Copy: operation started"), "Copy should report operation start");
apiCalls.length = 0;
refreshCalls.length = 0;
statusMessages.length = 0;
state.selectedTaskId = null;
await executeMoveSelection("storage1/dest");
assert(apiCalls.length === 1, "Multi-select move should issue one request");
assert(apiCalls[0].url === "/api/files/move", "Move should use move endpoint");
assert(Array.isArray(apiCalls[0].body.sources), "Move should send batch sources");
assert(apiCalls[0].body.sources.length === 2, "Move batch should include all selected items");
assert(apiCalls[0].body.destination_base === "storage1/dest", "Move batch should target destination base");
assert(state.selectedTaskId === "task-123", "Move should store the created task id");
assert(refreshCalls.length === 1, "Move should refresh task snapshot once");
assert(statusMessages.includes("Move: operation started"), "Move should report operation start");
assert(clearedSelection.length === 1 && clearedSelection[0].pane === "left", "Move batch should clear source selection once");
assert(errorCalls.length === 0, "Batch operation start should not emit action errors");
}})().catch((error) => {{
console.error(error);
process.exit(1);
}});
"""
)
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)
@@ -1014,6 +1131,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('startCopySelected();', app_js)
self.assertIn('openF6Flow();', app_js)
self.assertIn('deleteSelected();', app_js)
self.assertIn('sources: selectedItems.map((item) => item.path),', app_js)
self.assertIn('destination_base: baseDestination,', app_js)
self.assertIn('setStatus("Copy: operation started");', app_js)
self.assertIn('setStatus("Move: operation started");', app_js)
self.assertIn('const confirmed = await openConfirmModal({', app_js)
self.assertIn('title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?"', app_js)
self.assertIn('title: "Discard unsaved changes?"', app_js)
@@ -1108,6 +1229,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
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._run_operation_start_behavior_check(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)