feat: feedback verbetering 04
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||||
|
|||||||
+32
-42
@@ -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;
|
try {
|
||||||
let failures = 0;
|
let result;
|
||||||
let firstError = null;
|
if (selectedItems.length > 1) {
|
||||||
for (const item of selectedItems) {
|
result = await apiRequest("POST", "/api/files/copy", {
|
||||||
const destination = defaultDestination(item.path, baseDestination);
|
sources: selectedItems.map((item) => item.path),
|
||||||
try {
|
destination_base: baseDestination,
|
||||||
const result = await apiRequest("POST", "/api/files/copy", {
|
});
|
||||||
|
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,
|
||||||
});
|
});
|
||||||
state.selectedTaskId = result.task_id;
|
setStatus("Copy: started");
|
||||||
await refreshTasksSnapshot();
|
|
||||||
successes += 1;
|
|
||||||
} catch (err) {
|
|
||||||
failures += 1;
|
|
||||||
if (!firstError) {
|
|
||||||
firstError = `${item.path}: ${err.message}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
state.selectedTaskId = result.task_id;
|
||||||
|
await refreshTasksSnapshot();
|
||||||
|
} catch (err) {
|
||||||
|
setActionError("Copy", err);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
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,36 +2921,25 @@ 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;
|
const destination = defaultDestination(item.path, baseDestination);
|
||||||
let firstError = null;
|
if (item.kind !== "file") {
|
||||||
for (const item of selectedItems) {
|
setActionError("Move", new Error("Only files are supported for single-item move"));
|
||||||
const destination = defaultDestination(item.path, baseDestination);
|
return;
|
||||||
try {
|
|
||||||
if (item.kind !== "file") {
|
|
||||||
throw new Error("Only files are supported for move");
|
|
||||||
}
|
|
||||||
const result = await apiRequest("POST", "/api/files/move", {
|
|
||||||
source: item.path,
|
|
||||||
destination,
|
|
||||||
});
|
|
||||||
state.selectedTaskId = result.task_id;
|
|
||||||
await refreshTasksSnapshot();
|
|
||||||
successes += 1;
|
|
||||||
} catch (err) {
|
|
||||||
failures += 1;
|
|
||||||
if (!firstError) {
|
|
||||||
firstError = `${item.path}: ${err.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const result = await apiRequest("POST", "/api/files/move", {
|
||||||
|
source: item.path,
|
||||||
|
destination,
|
||||||
|
});
|
||||||
|
state.selectedTaskId = result.task_id;
|
||||||
|
await refreshTasksSnapshot();
|
||||||
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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user