feat: feedback verbetering - 06

This commit is contained in:
kodi
2026-03-15 15:51:13 +01:00
parent ae6a9d8c45
commit 9537a29de3
17 changed files with 368 additions and 37 deletions
@@ -368,7 +368,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
pollTimer: null,
lastRenderKey: "",
}};
const ACTIVE_OPERATION_OPERATIONS = new Set(["copy", "move", "duplicate"]);
const ACTIVE_OPERATION_OPERATIONS = new Set(["copy", "move", "duplicate", "delete"]);
const ACTIVE_TASK_STATUSES = new Set(["queued", "running", "cancelling"]);
{functions}
@@ -379,7 +379,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
{{ id: "c", operation: "download", status: "requested", source: "/src/c", destination: "kodidownload-20260315-120000.zip" }},
{{ id: "d", operation: "download", status: "preparing", source: "/src/d", destination: "folder.zip" }},
{{ id: "dup", operation: "duplicate", status: "queued", source: "/src/dup", destination: "/dst/dup" }},
{{ id: "del", operation: "delete", status: "running", source: "/src/del", destination: "" }},
{{ id: "del", operation: "delete", status: "running", source: "/src/del", destination: "", done_items: 2, total_items: 5, current_item: "folder/delete-me.txt" }},
{{ id: "stop", operation: "copy", status: "cancelling", source: "/src/stop", destination: "/dst/stop", done_items: 1, total_items: 4, current_item: "nested/final-file.txt" }},
{{ id: "e", operation: "copy", status: "completed", source: "/src/e", destination: "/dst/e" }},
{{ id: "f", operation: "move", status: "failed", source: "/src/f", destination: "/dst/f" }},
@@ -388,28 +388,33 @@ class UiSmokeGoldenTest(unittest.TestCase):
];
const activeTasks = activeTasksFromItems(mixedTasks);
assert(activeTasks.length === 4, "Only active user-visible operations should count as active");
assert(activeTasks.length === 5, "Only active user-visible operations should count as active");
assert(activeTasks.every((task) => isActiveTask(task)), "All filtered tasks should be active");
assert(!activeTasks.some((task) => task.operation === "delete"), "Delete should stay out of operation UI until it maps cleanly to one user-visible operation");
assert(activeTasks.some((task) => task.operation === "delete"), "Delete should be included once it maps cleanly to one user-visible operation");
assert(activeTasks.some((task) => task.status === "cancelling"), "Cancelling tasks should remain visible while stopping");
assert(activeTaskChipLabel(activeTasks) === "4 active operations", "Chip label should reflect active operation count");
assert(activeTaskChipLabel(activeTasks) === "5 active operations", "Chip label should reflect active operation count");
updateHeaderTaskState(mixedTasks);
assert(!elements["header-task-chip-container"].classList.contains("hidden"), "Chip should be visible with active tasks");
assert(elements["header-task-chip-label"].textContent === "4 active operations", "Chip label should render active operation count");
assert(elements["header-task-chip-label"].textContent === "5 active operations", "Chip label should render active operation count");
assert(shouldPollHeaderTasks(), "Active tasks should enable header polling");
setHeaderTaskPopoverOpen(true);
assert(headerTaskState.popoverOpen, "Popover should open when active tasks exist");
assert(!elements["header-task-popover"].classList.contains("hidden"), "Popover should be visible when open");
assert(elements["header-task-chip-btn"].attributes["aria-expanded"] === "true", "Chip button should expose expanded state");
assert(elements["header-task-popover-list"].children.length === 4, "Popover should render only active operations");
assert(elements["header-task-popover-list"].children.length === 5, "Popover should render only active operations");
const moveRow = elements["header-task-popover-list"].children[1];
const moveProgress = moveRow.children[3];
const moveCurrent = moveRow.children[4];
assert(moveProgress.textContent === "1/3", "Popover should show done/total progress when available");
assert(moveCurrent.textContent === "b.mkv", "Popover should show compact current item");
const cancellingRow = elements["header-task-popover-list"].children[3];
const deleteRow = elements["header-task-popover-list"].children[3];
const deleteProgress = deleteRow.children[3];
const deleteCurrent = deleteRow.children[4];
assert(deleteProgress.textContent === "2/5", "Delete operations should show done/total progress when available");
assert(deleteCurrent.textContent === "folder/delete-me.txt", "Delete operations should show compact current item");
const cancellingRow = elements["header-task-popover-list"].children[4];
const cancellingProgress = cancellingRow.children[3];
const cancellingCurrent = cancellingRow.children[4];
const cancellingSubtext = cancellingRow.children[5];
@@ -417,7 +422,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
assert(cancellingCurrent.textContent === "nested/final-file.txt", "Cancelling tasks should show current item");
assert(cancellingSubtext.textContent === "Stopping after current item...", "Cancelling tasks should explain stop semantics");
const firstActionButton = elements["header-task-popover-list"].children[0].children[3].children[0];
const cancellingActionButton = elements["header-task-popover-list"].children[3].children[6].children[0];
const cancellingActionButton = elements["header-task-popover-list"].children[4].children[6].children[0];
assert(firstActionButton.textContent === "Stop", "Queued/running tasks should expose a Stop action");
assert(!firstActionButton.disabled, "Queued/running tasks should be cancellable");
assert(cancellingActionButton.textContent === "Stopping...", "Cancelling tasks should show stopping state");
@@ -433,6 +438,11 @@ class UiSmokeGoldenTest(unittest.TestCase):
]);
assert(elements["header-task-chip-label"].textContent === "Duplicate 3/12", "Single duplicate task should show compact item progress in chip");
updateHeaderTaskState([
{{ id: "single-del", operation: "delete", status: "running", source: "/src/a", destination: "", done_items: 2, total_items: 5, current_item: "nested/file.txt" }},
]);
assert(elements["header-task-chip-label"].textContent === "Delete 2/5", "Single delete task should show compact item progress in chip");
updateHeaderTaskState([
{{ id: "single-move", operation: "move", status: "running", source: "/src/dir", destination: "/dst/dir", done_items: 0, total_items: 1, current_item: "Folder" }},
]);
@@ -582,7 +592,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
pollTimer: null,
lastRenderKey: "",
}};
const ACTIVE_OPERATION_OPERATIONS = new Set(["copy", "move", "duplicate"]);
const ACTIVE_OPERATION_OPERATIONS = new Set(["copy", "move", "duplicate", "delete"]);
const ACTIVE_TASK_STATUSES = new Set(["queued", "running", "cancelling"]);
{functions}
@@ -617,6 +627,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
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, "executeDeleteItems"),
self._extract_async_js_function(app_js, "startCopySelected"),
self._extract_async_js_function(app_js, "executeMoveSelection"),
]
@@ -693,6 +704,30 @@ class UiSmokeGoldenTest(unittest.TestCase):
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");
apiCalls.length = 0;
refreshCalls.length = 0;
statusMessages.length = 0;
loadCalls.length = 0;
clearedSelection.length = 0;
state.selectedTaskId = null;
await executeDeleteItems("left", [
{{ path: "storage1/source/a.txt", kind: "file", name: "a.txt" }},
{{ path: "storage1/source/folder", kind: "directory", name: "folder" }},
], new Set(["storage1/source/folder"]));
assert(apiCalls.length === 1, "Multi-select delete should issue one request");
assert(apiCalls[0].url === "/api/files/delete", "Delete should use delete endpoint");
assert(Array.isArray(apiCalls[0].body.paths), "Delete should send batch paths");
assert(apiCalls[0].body.paths.length === 2, "Delete batch should include all selected items");
assert(Array.isArray(apiCalls[0].body.recursive_paths), "Delete should send recursive path list");
assert(apiCalls[0].body.recursive_paths.length === 1, "Delete batch should include recursive selection paths");
assert(apiCalls[0].body.recursive_paths[0] === "storage1/source/folder", "Delete batch should preserve recursive path selection");
assert(state.selectedTaskId === "task-123", "Delete should store the created task id");
assert(refreshCalls.length === 1, "Delete should refresh task snapshot once");
assert(statusMessages.includes("Delete: operation started"), "Delete should report operation start");
assert(clearedSelection.length === 1 && clearedSelection[0].pane === "left", "Delete batch should clear source selection once");
assert(loadCalls.length === 1 && loadCalls[0] === "left", "Delete batch should reload the source pane once");
assert(errorCalls.length === 0, "Batch operation start should not emit action errors");
}})().catch((error) => {{
console.error(error);
@@ -965,7 +1000,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('function inferDownloadTaskContext(task)', app_js)
self.assertIn('function formatTaskLine(task)', app_js)
self.assertIn('let headerTaskState = {', app_js)
self.assertIn('const ACTIVE_OPERATION_OPERATIONS = new Set(["copy", "move", "duplicate"]);', app_js)
self.assertIn('const ACTIVE_OPERATION_OPERATIONS = new Set(["copy", "move", "duplicate", "delete"]);', app_js)
self.assertIn('const ACTIVE_TASK_STATUSES = new Set(["queued", "running", "cancelling"]);', app_js)
self.assertIn("The header chip/popover reflects user-visible file operations, not every task-backed file action.", app_js)
self.assertIn('function headerTaskElements()', app_js)
@@ -988,7 +1023,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('function updateHeaderTaskState(taskItems)', app_js)
self.assertIn('function applyTaskSnapshot(taskItems)', app_js)
self.assertIn('return `${count} active operation${count === 1 ? "" : "s"}`;', app_js)
self.assertIn('return task.operation === "copy" || task.operation === "duplicate";', app_js)
self.assertIn('return task.operation === "copy" || task.operation === "duplicate" || task.operation === "delete";', app_js)
self.assertIn('return `${action} ${task.done_items}/${task.total_items}`;', app_js)
self.assertIn('return `${action} running`;', app_js)
self.assertIn('return "Stopping after current item...";', app_js)
@@ -1081,7 +1116,10 @@ class UiSmokeGoldenTest(unittest.TestCase):
self.assertIn('function startContextMenuRename()', app_js)
self.assertIn('function startDuplicateSelected()', app_js)
self.assertIn('async function deleteSelected()', app_js)
self.assertIn('const result = await apiRequest("POST", "/api/files/delete", {', app_js)
self.assertIn('result = await apiRequest("POST", "/api/files/delete", {', app_js)
self.assertIn('paths: items.map((item) => item.path),', app_js)
self.assertIn('recursive_paths: Array.from(recursivePaths),', app_js)
self.assertIn('setStatus("Delete: operation started");', app_js)
self.assertIn('state.selectedTaskId = result.task_id;', app_js)
self.assertIn('await refreshTasksSnapshot();', app_js)
self.assertIn('function startContextMenuDuplicate()', app_js)