feat(tree): close rust family shell cutover
This commit is contained in:
@@ -21,6 +21,10 @@ use crate::tree_shell::renderer_input::{
|
||||
FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput, TreeShellCommandDispatcher,
|
||||
TreeShellRendererInput,
|
||||
};
|
||||
use crate::tree_shell::runtime_api::{
|
||||
TreeShellRuntimeRequest, TreeShellRuntimeResult,
|
||||
reduce_tree_shell_runtime as reduce_tree_shell_runtime_request,
|
||||
};
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderValue, StatusCode, header};
|
||||
@@ -1200,6 +1204,14 @@ fn build_tree_shell_html(
|
||||
typeof rendererInput.pageFocusKeyboardReducer === "object"
|
||||
? rendererInput.pageFocusKeyboardReducer
|
||||
: {};
|
||||
const runtimeArtifact =
|
||||
rendererInput.runtimeArtifact && typeof rendererInput.runtimeArtifact === "object"
|
||||
? rendererInput.runtimeArtifact
|
||||
: {};
|
||||
const runtimeApi =
|
||||
runtimeArtifact.runtimeApi && typeof runtimeArtifact.runtimeApi === "object"
|
||||
? runtimeArtifact.runtimeApi
|
||||
: {};
|
||||
const normalizeStringArray = (value) =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
@@ -1294,6 +1306,10 @@ fn build_tree_shell_html(
|
||||
const trimmed = value.trim();
|
||||
return trimmed || fallback;
|
||||
};
|
||||
const runtimeReduceEndpoint = normalizeText(
|
||||
runtimeApi.reduceEndpoint,
|
||||
"/api/tree/runtime/reduce",
|
||||
);
|
||||
|
||||
const normalizeParent = (value) => {
|
||||
const normalized = normalizeText(value);
|
||||
@@ -2237,8 +2253,34 @@ fn build_tree_shell_html(
|
||||
});
|
||||
};
|
||||
|
||||
const toggleExpand = (nodeId) => {
|
||||
const nextExpanded = !expanded.has(nodeId);
|
||||
const commitPageExpandedIds = (expandedIds) => {
|
||||
const nextExpanded = new Set(normalizeStringArray(expandedIds));
|
||||
let changed = nextExpanded.size !== expanded.size;
|
||||
if (!changed) {
|
||||
changed = Array.from(nextExpanded).some((nodeId) => !expanded.has(nodeId));
|
||||
}
|
||||
if (!changed) return false;
|
||||
expanded.clear();
|
||||
nextExpanded.forEach((nodeId) => expanded.add(nodeId));
|
||||
return true;
|
||||
};
|
||||
|
||||
const patchPageTreeAfterRuntimeState = (changedNodeIds, focusedId) => {
|
||||
if (mode !== "page") return;
|
||||
if (usedRustInitialRenderer) {
|
||||
const ids = normalizeStringArray(changedNodeIds);
|
||||
ids.forEach((nodeId) => {
|
||||
patchPageTreeExpansionDom(nodeId);
|
||||
});
|
||||
patchPageTreeActiveDom();
|
||||
if (focusedId) focusRowElement(focusedId);
|
||||
return;
|
||||
}
|
||||
renderTree();
|
||||
if (focusedId) focusRowElement(focusedId);
|
||||
};
|
||||
|
||||
const applyLocalPageExpansionFallback = (nodeId, nextExpanded) => {
|
||||
if (nextExpanded) expanded.add(nodeId);
|
||||
else expanded.delete(nodeId);
|
||||
postPageExpandChange(nodeId, nextExpanded);
|
||||
@@ -2249,6 +2291,10 @@ fn build_tree_shell_html(
|
||||
renderTree();
|
||||
};
|
||||
|
||||
const toggleExpand = (nodeId) => {
|
||||
applyLocalPageExpansionFallback(nodeId, !expanded.has(nodeId));
|
||||
};
|
||||
|
||||
const getVisiblePageItems = () => {
|
||||
const visible = [];
|
||||
const walk = (entries) => {
|
||||
@@ -2423,7 +2469,167 @@ fn build_tree_shell_html(
|
||||
focusRowElement(nodeId);
|
||||
};
|
||||
|
||||
const applyPageKeyboardAction = (action, item, sourceElement) => {
|
||||
const resolvePageActionItem = (action, item) => {
|
||||
const actionNodeId = normalizeText(action?.nodeId);
|
||||
if (actionNodeId && itemById.has(actionNodeId)) {
|
||||
return itemById.get(actionNodeId);
|
||||
}
|
||||
if (item?.nodeId && itemById.has(item.nodeId)) {
|
||||
return item;
|
||||
}
|
||||
return focusedNodeId && itemById.has(focusedNodeId)
|
||||
? itemById.get(focusedNodeId)
|
||||
: null;
|
||||
};
|
||||
|
||||
const buildPageRuntimeAction = (action, item) => {
|
||||
const actionKind = normalizeText(action?.kind).toLowerCase();
|
||||
if (actionKind === "focus") {
|
||||
const nodeId = normalizeText(action?.nodeId || item?.nodeId);
|
||||
return nodeId ? { kind: "focus", nodeId } : null;
|
||||
}
|
||||
if (actionKind === "move_next") return { kind: "moveNext" };
|
||||
if (actionKind === "move_previous") return { kind: "movePrevious" };
|
||||
if (actionKind === "move_home") return { kind: "moveHome" };
|
||||
if (actionKind === "move_end") return { kind: "moveEnd" };
|
||||
if (actionKind === "open") return { kind: "openFocused" };
|
||||
if (actionKind === "context_menu") return { kind: "contextMenuFocused" };
|
||||
if (actionKind === "expand" || actionKind === "collapse" || actionKind === "toggle") {
|
||||
const nodeId = normalizeText(action?.nodeId || item?.nodeId);
|
||||
return nodeId ? { kind: actionKind, nodeId } : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildPageRuntimeEnvironment = () => ({
|
||||
visibleNodeIds: getVisiblePageItems().map((entry) => entry.nodeId),
|
||||
expandableNodeIds: normalizedItems
|
||||
.filter((entry) => entry.childCount > 0 && getSiblings(entry.nodeId).length > 0)
|
||||
.map((entry) => entry.nodeId),
|
||||
});
|
||||
|
||||
const readPageRuntimeState = (action, item) => {
|
||||
const actionKind = normalizeText(action?.kind).toLowerCase();
|
||||
const nodeId = normalizeText(action?.nodeId || item?.nodeId);
|
||||
const focusedId =
|
||||
(actionKind === "open" || actionKind === "context_menu") && itemById.has(nodeId)
|
||||
? nodeId
|
||||
: focusedNodeId || null;
|
||||
return {
|
||||
focusedId,
|
||||
expandedIds: Array.from(expanded),
|
||||
dropFeedback: null,
|
||||
};
|
||||
};
|
||||
|
||||
const reducePageActionWithRuntime = async (action, item) => {
|
||||
if (mode !== "page" || !runtimeReduceEndpoint) {
|
||||
return null;
|
||||
}
|
||||
const runtimeAction = buildPageRuntimeAction(action, item);
|
||||
if (!runtimeAction) return null;
|
||||
const response = await fetch(runtimeReduceEndpoint, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
mode: "page",
|
||||
requestId: `page-runtime-${Date.now()}`,
|
||||
environment: buildPageRuntimeEnvironment(),
|
||||
state: readPageRuntimeState(action, item),
|
||||
action: runtimeAction,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response));
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const normalizePageRuntimeResult = (runtimeResult) => {
|
||||
if (!runtimeResult || runtimeResult.mode !== "page") {
|
||||
return null;
|
||||
}
|
||||
const stateSnapshot =
|
||||
runtimeResult.state && runtimeResult.state.mode === "page"
|
||||
? runtimeResult.state.state
|
||||
: null;
|
||||
const pagePatch = Array.isArray(runtimeResult.domPatches)
|
||||
? runtimeResult.domPatches.find((patch) => patch?.kind === "pageState")
|
||||
: null;
|
||||
const focusedId =
|
||||
typeof pagePatch?.focusedId === "string"
|
||||
? pagePatch.focusedId
|
||||
: typeof stateSnapshot?.focusedId === "string"
|
||||
? stateSnapshot.focusedId
|
||||
: "";
|
||||
const expandedIds = Array.isArray(pagePatch?.expandedIds)
|
||||
? pagePatch.expandedIds
|
||||
: Array.isArray(stateSnapshot?.expandedIds)
|
||||
? stateSnapshot.expandedIds
|
||||
: null;
|
||||
return {
|
||||
focusedId: normalizeText(focusedId),
|
||||
expandedIds: expandedIds ? normalizeStringArray(expandedIds) : null,
|
||||
hostEvents: Array.isArray(runtimeResult.hostEvents) ? runtimeResult.hostEvents : [],
|
||||
};
|
||||
};
|
||||
|
||||
const replayPageRuntimeHostEvents = (runtimeResult, item, sourceElement) => {
|
||||
const result = normalizePageRuntimeResult(runtimeResult);
|
||||
if (!result) return false;
|
||||
let replayed = false;
|
||||
result.hostEvents.forEach((event) => {
|
||||
if (!event || typeof event !== "object") return;
|
||||
if (event.kind === "pageOpen") {
|
||||
const nodeId = normalizeText(event.nodeId);
|
||||
if (nodeId) {
|
||||
handleNavigate(nodeId);
|
||||
replayed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.kind === "pageContextMenu") {
|
||||
const nodeId = normalizeText(event.nodeId || item?.nodeId);
|
||||
if (!nodeId) return;
|
||||
const rect = sourceElement?.getBoundingClientRect?.();
|
||||
openContextMenu(
|
||||
nodeId,
|
||||
rect ? rect.left + Math.min(rect.width - 12, 28) : 0,
|
||||
rect ? rect.top + Math.min(rect.height - 12, 18) : 0,
|
||||
);
|
||||
replayed = true;
|
||||
}
|
||||
});
|
||||
return replayed;
|
||||
};
|
||||
|
||||
const reconcilePageRuntimeResult = (runtimeResult, item) => {
|
||||
const result = normalizePageRuntimeResult(runtimeResult);
|
||||
if (!result) return false;
|
||||
const previousExpanded = new Set(expanded);
|
||||
let shouldPatchTree = false;
|
||||
if (Array.isArray(result.expandedIds)) {
|
||||
shouldPatchTree = commitPageExpandedIds(result.expandedIds) || shouldPatchTree;
|
||||
}
|
||||
if (result.focusedId && result.focusedId !== focusedNodeId) {
|
||||
focusedNodeId = result.focusedId;
|
||||
postPageFocusChange(result.focusedId);
|
||||
shouldPatchTree = true;
|
||||
}
|
||||
const itemNodeId = normalizeText(item?.nodeId);
|
||||
if (itemNodeId && previousExpanded.has(itemNodeId) !== expanded.has(itemNodeId)) {
|
||||
postPageExpandChange(itemNodeId, expanded.has(itemNodeId));
|
||||
}
|
||||
if (shouldPatchTree) {
|
||||
patchPageTreeAfterRuntimeState(
|
||||
Array.isArray(result.expandedIds) ? [itemNodeId, ...result.expandedIds] : [itemNodeId],
|
||||
result.focusedId || focusedNodeId,
|
||||
);
|
||||
}
|
||||
return shouldPatchTree;
|
||||
};
|
||||
|
||||
const applyLocalPageActionFallback = (action, item, sourceElement) => {
|
||||
if (mode !== "page") return;
|
||||
const actionKind = normalizeText(action?.kind).toLowerCase();
|
||||
if (!pageFocusKeyboardReducerActions.has(actionKind)) {
|
||||
@@ -2467,13 +2673,7 @@ fn build_tree_shell_html(
|
||||
}
|
||||
if (actionKind === "expand") {
|
||||
if (item?.childCount > 0 && !expanded.has(item.nodeId)) {
|
||||
expanded.add(item.nodeId);
|
||||
postPageExpandChange(item.nodeId, true);
|
||||
if (usedRustInitialRenderer) {
|
||||
patchPageTreeExpansionDom(item.nodeId);
|
||||
} else {
|
||||
renderTree();
|
||||
}
|
||||
applyLocalPageExpansionFallback(item.nodeId, true);
|
||||
focusRowElement(item.nodeId);
|
||||
return;
|
||||
}
|
||||
@@ -2485,13 +2685,7 @@ fn build_tree_shell_html(
|
||||
}
|
||||
if (actionKind === "collapse") {
|
||||
if (item?.childCount > 0 && expanded.has(item.nodeId)) {
|
||||
expanded.delete(item.nodeId);
|
||||
postPageExpandChange(item.nodeId, false);
|
||||
if (usedRustInitialRenderer) {
|
||||
patchPageTreeExpansionDom(item.nodeId);
|
||||
} else {
|
||||
renderTree();
|
||||
}
|
||||
applyLocalPageExpansionFallback(item.nodeId, false);
|
||||
focusRowElement(item.nodeId);
|
||||
return;
|
||||
}
|
||||
@@ -2522,6 +2716,35 @@ fn build_tree_shell_html(
|
||||
}
|
||||
};
|
||||
|
||||
const applyPageKeyboardAction = (action, item, sourceElement) => {
|
||||
if (mode !== "page") return;
|
||||
const actionKind = normalizeText(action?.kind).toLowerCase();
|
||||
if (!pageFocusKeyboardReducerActions.has(actionKind)) {
|
||||
return;
|
||||
}
|
||||
const runtimeItem = resolvePageActionItem(action, item);
|
||||
const runtimeAction = buildPageRuntimeAction(action, runtimeItem);
|
||||
if (!runtimeAction) {
|
||||
applyLocalPageActionFallback(action, runtimeItem || item, sourceElement);
|
||||
return;
|
||||
}
|
||||
void reducePageActionWithRuntime(action, runtimeItem)
|
||||
.then((runtimeResult) => {
|
||||
const replayedHostEvent = replayPageRuntimeHostEvents(
|
||||
runtimeResult,
|
||||
runtimeItem,
|
||||
sourceElement,
|
||||
);
|
||||
const reconciledState = reconcilePageRuntimeResult(runtimeResult, runtimeItem);
|
||||
if (!replayedHostEvent && !reconciledState) {
|
||||
applyLocalPageActionFallback(action, runtimeItem || item, sourceElement);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
applyLocalPageActionFallback(action, runtimeItem || item, sourceElement);
|
||||
});
|
||||
};
|
||||
|
||||
const postPickerFocusChange = (pickerItemKey) => {
|
||||
if (mode !== "picker") return;
|
||||
const normalizedItemKey = normalizeText(pickerItemKey);
|
||||
@@ -2976,16 +3199,15 @@ fn build_tree_shell_html(
|
||||
const action = normalizeText(element.dataset.rustAction);
|
||||
if (action === "toggle") {
|
||||
event.preventDefault();
|
||||
toggleExpand(item.nodeId);
|
||||
applyPageKeyboardAction({ kind: "toggle", nodeId: item.nodeId }, item, element);
|
||||
} else if (action === "open") {
|
||||
handleNavigate(item.nodeId);
|
||||
applyPageKeyboardAction({ kind: "open", nodeId: item.nodeId }, item, element);
|
||||
} else if (action === "create") {
|
||||
void handleCreate(item.nodeId);
|
||||
} else if (action === "rename") {
|
||||
void handleRename(item.nodeId);
|
||||
} else if (action === "menu") {
|
||||
const center = getElementCenter(element);
|
||||
openContextMenu(item.nodeId, center.x, center.y);
|
||||
applyPageKeyboardAction({ kind: "context_menu", nodeId: item.nodeId }, item, element);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -3524,7 +3746,7 @@ fn build_tree_shell_html(
|
||||
toggleButton.textContent = expanded.has(item.nodeId) ? "▾" : "▸";
|
||||
toggleButton.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
toggleExpand(item.nodeId);
|
||||
applyPageKeyboardAction({ kind: "toggle", nodeId: item.nodeId }, item, event.currentTarget);
|
||||
});
|
||||
row.appendChild(toggleButton);
|
||||
} else {
|
||||
@@ -4377,6 +4599,12 @@ pub async fn tree_command(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn reduce_tree_shell_runtime(
|
||||
Json(body): Json<TreeShellRuntimeRequest>,
|
||||
) -> Json<TreeShellRuntimeResult> {
|
||||
Json(reduce_tree_shell_runtime_request(body))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{TreeCommandRequest, create_command_wire};
|
||||
@@ -4441,6 +4669,19 @@ mod tests {
|
||||
assert!(html.contains("tree.shell.state.patch"));
|
||||
assert!(html.contains("\"contractName\":\"rust_page_focus_keyboard_reducer_v1\""));
|
||||
assert!(html.contains("applyPageKeyboardAction"));
|
||||
assert!(html.contains("reducePageActionWithRuntime"));
|
||||
let apply_page_action_start = html
|
||||
.find("const applyPageKeyboardAction = (action, item, sourceElement) => {")
|
||||
.expect("applyPageKeyboardAction should be embedded");
|
||||
let apply_page_action_end = html[apply_page_action_start..]
|
||||
.find("\n const postPickerFocusChange")
|
||||
.expect("applyPageKeyboardAction should end before picker focus handler");
|
||||
let apply_page_action_body =
|
||||
&html[apply_page_action_start..apply_page_action_start + apply_page_action_end];
|
||||
assert!(
|
||||
!apply_page_action_body.contains("toggleExpand("),
|
||||
"page keyboard/expand should prefer runtime result instead of directly toggling local expansion state"
|
||||
);
|
||||
assert!(html.contains("patchPageTreeActiveDom"));
|
||||
assert!(html.contains("patchPageTreeExpansionDom"));
|
||||
assert!(html.contains("data-rust-page-renderer=\"initial_v1\""));
|
||||
@@ -4550,7 +4791,16 @@ mod tests {
|
||||
assert!(filetree_html.contains("\"commandDispatcher\""));
|
||||
assert!(filetree_html.contains("\"runtimeArtifact\""));
|
||||
assert!(filetree_html.contains("\"contractName\":\"rust_tree_shell_runtime_artifact_v1\""));
|
||||
assert!(filetree_html.contains("\"outputChannels\":[\"commandDispatchEvent\",\"domPatch\",\"intentEvent\"]"));
|
||||
assert!(filetree_html.contains("\"reduceEndpoint\":\"/api/tree/runtime/reduce\""));
|
||||
assert!(filetree_html.contains(
|
||||
"\"wasmModuleUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm\""
|
||||
));
|
||||
assert!(filetree_html.contains(
|
||||
"\"jsGlueUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime.js\""
|
||||
));
|
||||
assert!(filetree_html.contains(
|
||||
"\"outputChannels\":[\"commandDispatchEvent\",\"domPatch\",\"intentEvent\"]"
|
||||
));
|
||||
|
||||
let picker_response = app()
|
||||
.oneshot(
|
||||
@@ -4571,6 +4821,146 @@ mod tests {
|
||||
assert!(picker_html.contains("\"excludedPickerIds\":[\"page_root\"]"));
|
||||
assert!(picker_html.contains("\"runtimeArtifact\""));
|
||||
assert!(picker_html.contains("\"contractName\":\"rust_tree_shell_runtime_artifact_v1\""));
|
||||
assert!(picker_html.contains("\"reduceEndpoint\":\"/api/tree/runtime/reduce\""));
|
||||
assert!(picker_html.contains(
|
||||
"\"wasmModuleUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime_bg.wasm\""
|
||||
));
|
||||
assert!(picker_html.contains(
|
||||
"\"jsGlueUrl\":\"/api/tree-shell-runtime/mnote-tree-shell-runtime.js\""
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_runtime_reduce_endpoint_returns_filetree_runtime_result() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/tree/runtime/reduce")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"mode":"fileTree","requestId":"req-filetree-route","environment":{"visibleRowIds":["doc:root","asset:image"],"rows":[{"rowId":"doc:root","rowKind":"doc","documentId":"root","assetId":null},{"rowId":"asset:image","rowKind":"asset","documentId":"root","assetId":"image"}]},"state":{"selection":{"selectedRowIds":[],"anchorRowId":null,"focusedRowId":null},"dragRowIds":[],"dragEffect":null,"dropTargetRowId":null},"action":{"kind":"selectRow","rowId":"asset:image","modifiers":{"shiftKey":false,"ctrlKey":false,"metaKey":false}}}"#,
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
|
||||
assert_eq!(payload["mode"], Value::String("fileTree".into()));
|
||||
assert_eq!(
|
||||
payload["requestId"],
|
||||
Value::String("req-filetree-route".into())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["domPatches"][0]["kind"],
|
||||
Value::String("fileTreeState".into())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["domPatches"][0]["selectedRowIds"][0],
|
||||
Value::String("asset:image".into())
|
||||
);
|
||||
|
||||
let drop_target_response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/tree/runtime/reduce")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"mode":"fileTree","requestId":"req-filetree-drop-target-route","environment":{"visibleRowIds":["doc:root","asset:image"],"rows":[{"rowId":"doc:root","rowKind":"doc","documentId":"root","assetId":null},{"rowId":"asset:image","rowKind":"asset","documentId":"root","assetId":"image"}]},"state":{"selection":{"selectedRowIds":[],"anchorRowId":null,"focusedRowId":null},"dragRowIds":[],"dragEffect":null,"dropTargetRowId":null},"action":{"kind":"updateDropTarget","rowId":"asset:image"}}"#,
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(drop_target_response.status(), StatusCode::OK);
|
||||
let drop_target_body = axum::body::to_bytes(drop_target_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let drop_target_payload: Value = serde_json::from_slice(&drop_target_body).expect("json");
|
||||
|
||||
assert_eq!(
|
||||
drop_target_payload["domPatches"][0]["dropTargetRowId"],
|
||||
Value::String("asset:image".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_runtime_reduce_endpoint_returns_page_and_picker_runtime_results() {
|
||||
let page_response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/tree/runtime/reduce")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"mode":"page","requestId":"req-page-route","environment":{"visibleNodeIds":["doc:root","doc:child"],"expandableNodeIds":["doc:root"]},"state":{"focusedId":"doc:root","expandedIds":[],"dropFeedback":null},"action":{"kind":"moveNext"}}"#,
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(page_response.status(), StatusCode::OK);
|
||||
let page_body = axum::body::to_bytes(page_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let page_payload: Value = serde_json::from_slice(&page_body).expect("json");
|
||||
|
||||
assert_eq!(page_payload["mode"], Value::String("page".into()));
|
||||
assert_eq!(
|
||||
page_payload["requestId"],
|
||||
Value::String("req-page-route".into())
|
||||
);
|
||||
assert_eq!(
|
||||
page_payload["domPatches"][0]["kind"],
|
||||
Value::String("pageState".into())
|
||||
);
|
||||
assert_eq!(
|
||||
page_payload["domPatches"][0]["focusedId"],
|
||||
Value::String("doc:child".into())
|
||||
);
|
||||
|
||||
let picker_response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/tree/runtime/reduce")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"mode":"picker","requestId":"req-picker-route","environment":{"items":[{"itemKey":"doc:root","documentId":"doc:root","pickable":true},{"itemKey":"doc:child","documentId":"doc:child","pickable":true}],"excludedIds":[],"allowRootPick":false},"state":{"activeItemKey":"doc:child"},"action":{"kind":"pick"}}"#,
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(picker_response.status(), StatusCode::OK);
|
||||
let picker_body = axum::body::to_bytes(picker_response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let picker_payload: Value = serde_json::from_slice(&picker_body).expect("json");
|
||||
|
||||
assert_eq!(picker_payload["mode"], Value::String("picker".into()));
|
||||
assert_eq!(
|
||||
picker_payload["requestId"],
|
||||
Value::String("req-picker-route".into())
|
||||
);
|
||||
assert_eq!(
|
||||
picker_payload["hostEvents"][0]["kind"],
|
||||
Value::String("pickerPickDocument".into())
|
||||
);
|
||||
assert_eq!(
|
||||
picker_payload["hostEvents"][0]["documentId"],
|
||||
Value::String("doc:child".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user