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] return source[start : index + 1]
self.fail(f"Expected closing brace for function {name}") 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: def _run_app_js_behavior_check(self, app_js: str) -> None:
functions = "\n\n".join( functions = "\n\n".join(
[ [
@@ -592,6 +611,104 @@ class UiSmokeGoldenTest(unittest.TestCase):
) )
self.assertEqual(result.returncode, 0, msg=result.stderr or result.stdout) 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: def test_ui_mount_and_index_contains_expected_panels(self) -> None:
mount = self._ui_mount() mount = self._ui_mount()
self.assertIsInstance(mount.app, StaticFiles) self.assertIsInstance(mount.app, StaticFiles)
@@ -1014,6 +1131,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('startCopySelected();', app_js) self.assertIn('startCopySelected();', app_js)
self.assertIn('openF6Flow();', app_js) self.assertIn('openF6Flow();', app_js)
self.assertIn('deleteSelected();', 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('const confirmed = await openConfirmModal({', app_js)
self.assertIn('title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?"', app_js) self.assertIn('title: selectedItems.length === 1 ? "Delete item?" : "Delete selected items?"', app_js)
self.assertIn('title: "Discard unsaved changes?"', 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('input.setAttribute("webkitdirectory", "")', app_js)
self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js) self.assertIn('await apiRequest("POST", "/api/files/mkdir", {', app_js)
self.assertIn('await uploadFileRequest(targetPath, entry.file, overwrite);', 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: preparing', app_js)
self.assertIn('Folder upload: ${uploadState.successfulCount} uploaded, ${uploadState.skippedCount} skipped', app_js) self.assertIn('Folder upload: ${uploadState.successfulCount} uploaded, ${uploadState.skippedCount} skipped', app_js)
self.assertIn('async function handleUploadSelection(event)', app_js) self.assertIn('async function handleUploadSelection(event)', app_js)
+21 -31
View File
@@ -2874,28 +2874,30 @@ async function startCopySelected() {
} }
const baseDestination = paneState(destinationPane).currentPath; const baseDestination = paneState(destinationPane).currentPath;
setError("actions-error", ""); setError("actions-error", "");
let successes = 0;
let failures = 0;
let firstError = null;
for (const item of selectedItems) {
const destination = defaultDestination(item.path, baseDestination);
try { try {
const result = await apiRequest("POST", "/api/files/copy", { let result;
if (selectedItems.length > 1) {
result = await apiRequest("POST", "/api/files/copy", {
sources: selectedItems.map((item) => item.path),
destination_base: baseDestination,
});
setStatus("Copy: operation started");
} else {
const item = selectedItems[0];
const destination = defaultDestination(item.path, baseDestination);
result = await apiRequest("POST", "/api/files/copy", {
source: item.path, source: item.path,
destination, destination,
}); });
setStatus("Copy: started");
}
state.selectedTaskId = result.task_id; state.selectedTaskId = result.task_id;
await refreshTasksSnapshot(); await refreshTasksSnapshot();
successes += 1;
} catch (err) { } catch (err) {
failures += 1; setActionError("Copy", err);
if (!firstError) { return;
firstError = `${item.path}: ${err.message}`;
}
}
} }
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]); await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
showActionSummary("Copy", successes, failures, firstError);
} }
async function startMoveSelected() { async function startMoveSelected() {
@@ -2908,10 +2910,9 @@ async function executeMoveSelection(baseDestination) {
if (selectedItems.length === 0) { if (selectedItems.length === 0) {
return; return;
} }
const allFiles = selectedItems.every((item) => item.kind === "file");
setError("actions-error", ""); setError("actions-error", "");
if (!allFiles) { if (selectedItems.length > 1) {
const result = await apiRequest("POST", "/api/files/move", { const result = await apiRequest("POST", "/api/files/move", {
sources: selectedItems.map((item) => item.path), sources: selectedItems.map((item) => item.path),
destination_base: baseDestination, destination_base: baseDestination,
@@ -2920,18 +2921,15 @@ async function executeMoveSelection(baseDestination) {
await refreshTasksSnapshot(); await refreshTasksSnapshot();
setSelectedItem(sourcePane, null); setSelectedItem(sourcePane, null);
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]); await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
setStatus("Move: batch started"); setStatus("Move: operation started");
return; return;
} }
let successes = 0; const item = selectedItems[0];
let failures = 0;
let firstError = null;
for (const item of selectedItems) {
const destination = defaultDestination(item.path, baseDestination); const destination = defaultDestination(item.path, baseDestination);
try {
if (item.kind !== "file") { if (item.kind !== "file") {
throw new Error("Only files are supported for move"); setActionError("Move", new Error("Only files are supported for single-item move"));
return;
} }
const result = await apiRequest("POST", "/api/files/move", { const result = await apiRequest("POST", "/api/files/move", {
source: item.path, source: item.path,
@@ -2939,17 +2937,9 @@ async function executeMoveSelection(baseDestination) {
}); });
state.selectedTaskId = result.task_id; state.selectedTaskId = result.task_id;
await refreshTasksSnapshot(); await refreshTasksSnapshot();
successes += 1;
} catch (err) {
failures += 1;
if (!firstError) {
firstError = `${item.path}: ${err.message}`;
}
}
}
setSelectedItem(sourcePane, null); setSelectedItem(sourcePane, null);
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]); await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
showActionSummary("Move", successes, failures, firstError); setStatus("Move: started");
} }
async function addBookmark() { async function addBookmark() {