feat(tree): complete rust family runtime checklist

- add tree shell runtime artifact contracts and page/filetree/picker runtime reducers
- sink tree.subtree.move write operation through Rust and formalize command event plans
- harden file tree search projection contract and route thin-proxy boundaries
- record completed harness tasks and move design docs into process/done
This commit is contained in:
lix-2026
2026-04-27 10:27:15 +08:00
parent e564dfde02
commit 4ab36a9386
30 changed files with 2502 additions and 432 deletions
+145 -34
View File
@@ -2242,6 +2242,10 @@ fn build_tree_shell_html(
if (nextExpanded) expanded.add(nodeId);
else expanded.delete(nodeId);
postPageExpandChange(nodeId, nextExpanded);
if (mode === "page" && usedRustInitialRenderer && patchPageTreeExpansionDom(nodeId)) {
focusRowElement(nodeId);
return;
}
renderTree();
};
@@ -2410,6 +2414,11 @@ fn build_tree_shell_html(
}
focusedNodeId = nodeId;
postPageFocusChange(nodeId);
if (usedRustInitialRenderer) {
patchPageTreeActiveDom();
focusRowElement(nodeId);
return;
}
renderTree();
focusRowElement(nodeId);
};
@@ -2460,7 +2469,11 @@ fn build_tree_shell_html(
if (item?.childCount > 0 && !expanded.has(item.nodeId)) {
expanded.add(item.nodeId);
postPageExpandChange(item.nodeId, true);
renderTree();
if (usedRustInitialRenderer) {
patchPageTreeExpansionDom(item.nodeId);
} else {
renderTree();
}
focusRowElement(item.nodeId);
return;
}
@@ -2474,7 +2487,11 @@ fn build_tree_shell_html(
if (item?.childCount > 0 && expanded.has(item.nodeId)) {
expanded.delete(item.nodeId);
postPageExpandChange(item.nodeId, false);
renderTree();
if (usedRustInitialRenderer) {
patchPageTreeExpansionDom(item.nodeId);
} else {
renderTree();
}
focusRowElement(item.nodeId);
return;
}
@@ -2524,12 +2541,13 @@ fn build_tree_shell_html(
});
};
const applyPickerFocusByItemKey = (pickerItemKey) => {
const applyPickerFocusByItemKey = (pickerItemKey, options = {}) => {
if (mode !== "picker") return;
const result = computePickerStateActionResult({
kind: "focus",
itemKey: pickerItemKey,
});
const shouldFocusDom = options.focusDom === true;
const nextPickerItemKey = result.nextItemKey || "";
const nextDocumentId =
nextPickerItemKey && nextPickerItemKey !== "__root__"
@@ -2541,11 +2559,11 @@ fn build_tree_shell_html(
focusedNodeId = nextDocumentId || "";
if (usedRustInitialRenderer) {
patchPickerActiveDom();
focusPickerRowElement(nextPickerItemKey);
if (shouldFocusDom) focusPickerRowElement(nextPickerItemKey);
} else {
renderTree();
}
if (nextDocumentId) {
if (shouldFocusDom && nextDocumentId) {
focusRowElement(nextDocumentId);
}
postPickerFocusChange(nextPickerItemKey || null);
@@ -2567,6 +2585,27 @@ fn build_tree_shell_html(
return result;
};
const postPickerPickResultToHost = (result) => {
if (mode !== "picker" || !result) return;
if (result.pickedRoot) {
setLastAction("已选择根目录");
postToHost("tree.pick.root", {
documentId: null,
target: { documentId: null },
payload: { documentId: null },
});
return;
}
if (result.pickedDocumentId) {
postToHost("tree.pick", {
documentId: result.pickedDocumentId,
itemKey: result.nextItemKey || result.pickedDocumentId,
target: { documentId: result.pickedDocumentId },
payload: { documentId: result.pickedDocumentId },
});
}
};
const handlePickerCommand = (command) => {
if (mode !== "picker") return;
@@ -2576,19 +2615,7 @@ fn build_tree_shell_html(
}
if (normalizedCommand === "pick") {
const result = applyPickerStateAction({ kind: "pick" });
if (result.pickedRoot) {
setLastAction("已选择根目录");
postToHost("tree.pick.root", {
documentId: null,
target: { documentId: null },
payload: { documentId: null },
});
return;
}
if (result.pickedDocumentId) {
handleNavigate(result.pickedDocumentId);
}
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
return;
}
@@ -2947,7 +2974,10 @@ fn build_tree_shell_html(
element.addEventListener("click", (event) => {
event.stopPropagation();
const action = normalizeText(element.dataset.rustAction);
if (action === "open") {
if (action === "toggle") {
event.preventDefault();
toggleExpand(item.nodeId);
} else if (action === "open") {
handleNavigate(item.nodeId);
} else if (action === "create") {
void handleCreate(item.nodeId);
@@ -2961,6 +2991,72 @@ fn build_tree_shell_html(
});
};
const patchPageTreeActiveDom = () => {
if (mode !== "page") return;
appElement.querySelectorAll('[data-rust-rendered-row="page"]').forEach((row) => {
if (!(row instanceof HTMLElement)) return;
const nodeId = normalizeText(row.dataset.nodeId);
const isFocused = nodeId === focusedNodeId;
row.dataset.active = String(nodeId === currentActiveDocumentId);
row.dataset.focused = String(isFocused);
row.tabIndex = isFocused ? 0 : -1;
row.dataset.dropFeedback = String(activePageDropNodeId === nodeId);
});
};
const patchPageTreeExpansionDom = (nodeId) => {
if (mode !== "page") return false;
const normalizedNodeId = normalizeText(nodeId);
if (!normalizedNodeId) return false;
const item = itemById.get(normalizedNodeId);
if (!item) return false;
const nodeElement = appElement.querySelector(
`.tree-node[data-node-id="${CSS.escape(normalizedNodeId)}"]`,
);
if (!(nodeElement instanceof HTMLElement)) return false;
const row = nodeElement.querySelector(
`:scope > .tree-row[data-node-id="${CSS.escape(normalizedNodeId)}"]`,
);
const children = getSiblings(normalizedNodeId);
const hasChildren = item.childCount > 0 && children.length > 0;
const isExpanded = hasChildren && expanded.has(normalizedNodeId);
if (row instanceof HTMLElement) {
row.setAttribute("aria-expanded", hasChildren ? String(isExpanded) : "false");
const toggleButton = row.querySelector('[data-testid="tree-node-toggle"]');
if (toggleButton instanceof HTMLButtonElement) {
toggleButton.textContent = isExpanded ? "▾" : "▸";
toggleButton.setAttribute(
"aria-label",
`${isExpanded ? "折叠" : "展开"} ${item.title}`,
);
}
}
if (!hasChildren) {
patchPageTreeActiveDom();
return true;
}
let childrenList = Array.from(nodeElement.children).find(
(child) => child instanceof HTMLElement && child.classList.contains("tree-children"),
);
if (isExpanded) {
if (!(childrenList instanceof HTMLElement)) {
childrenList = document.createElement("ul");
childrenList.className = "tree-children";
children.forEach((child) => {
childrenList.appendChild(renderNode(child));
});
nodeElement.appendChild(childrenList);
}
childrenList.hidden = false;
childrenList.style.display = "";
} else if (childrenList instanceof HTMLElement) {
childrenList.hidden = true;
childrenList.style.display = "none";
}
patchPageTreeActiveDom();
return true;
};
const hydrateInitialPageTree = () => {
if (mode !== "page") return false;
const root = appElement.querySelector('[data-rust-page-renderer="initial_v1"]');
@@ -3210,12 +3306,8 @@ fn build_tree_shell_html(
if (!(row instanceof HTMLElement)) return;
row.dataset.focused = String(resolvePickerRootFocused());
row.addEventListener("click", () => {
setLastAction("已选择根目录");
postToHost("tree.pick.root", {
documentId: null,
target: { documentId: null },
payload: { documentId: null },
});
applyPickerFocusByItemKey("__root__", { focusDom: true });
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
});
};
@@ -3228,7 +3320,8 @@ fn build_tree_shell_html(
(!currentActivePickerItemKey && currentActiveDocumentId === item.nodeId),
);
row.addEventListener("click", () => {
handleNavigate(item.nodeId);
applyPickerFocusByItemKey(item.nodeId, { focusDom: true });
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
});
};
@@ -3447,7 +3540,14 @@ fn build_tree_shell_html(
linkButton.className = "tree-link";
linkButton.setAttribute("data-testid", "tree-node-open");
linkButton.setAttribute("aria-label", `打开 ${item.title}`);
linkButton.addEventListener("click", () => handleNavigate(item.nodeId));
linkButton.addEventListener("click", () => {
if (mode === "picker") {
applyPickerFocusByItemKey(item.nodeId, { focusDom: true });
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
return;
}
handleNavigate(item.nodeId);
});
const titleElement = document.createElement("span");
titleElement.className = "tree-link-title";
@@ -3812,13 +3912,10 @@ fn build_tree_shell_html(
rootButton.className = "tree-row";
rootButton.setAttribute("data-testid", "tree-picker-root");
rootButton.dataset.focused = String(resolvePickerRootFocused());
rootButton.tabIndex = resolvePickerRootFocused() ? 0 : -1;
rootButton.addEventListener("click", () => {
setLastAction("已选择根目录");
postToHost("tree.pick.root", {
documentId: null,
target: { documentId: null },
payload: { documentId: null },
});
applyPickerFocusByItemKey("__root__", { focusDom: true });
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
});
const spacer = document.createElement("div");
@@ -3903,7 +4000,6 @@ fn build_tree_shell_html(
focusedNodeId = resolveFocusedNodeIdFromHostState();
if (mode === "picker" && usedRustInitialRenderer) {
patchPickerActiveDom();
focusPickerRowElement(currentActivePickerItemKey);
return;
}
renderTree();
@@ -4345,8 +4441,12 @@ 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("patchPageTreeActiveDom"));
assert!(html.contains("patchPageTreeExpansionDom"));
assert!(html.contains("data-rust-page-renderer=\"initial_v1\""));
assert!(html.contains("data-rust-rendered-row=\"page\""));
assert!(html.contains("data-rust-action=\"toggle\""));
assert!(html.contains("data-testid=\"tree-node-toggle\""));
assert!(html.contains("hydrateInitialPageTree"));
assert!(html.contains("const usedRustInitialPageRenderer = hydrateInitialPageTree();"));
assert!(html.contains("application/x-mnote-page-tree-node"));
@@ -4382,8 +4482,14 @@ mod tests {
assert!(html.contains("tree.picker.focus.changed"));
assert!(html.contains("\"contractName\":\"rust_picker_state_reducer_v1\""));
assert!(html.contains("applyPickerStateAction"));
assert!(html.contains("postPickerPickResultToHost"));
assert!(html.contains("applyPickerFocusByItemKey(\"__root__\", { focusDom: true })"));
assert!(html.contains("applyPickerFocusByItemKey(item.nodeId, { focusDom: true })"));
assert!(html.contains("const shouldFocusDom = options.focusDom === true"));
assert!(html.contains("if (shouldFocusDom) focusPickerRowElement"));
assert!(html.contains("patchPickerActiveDom"));
assert!(html.contains("hydrateInitialPickerTree"));
assert!(html.contains("tabindex=\""));
assert!(html.contains("const usedRustInitialRenderer = hydrateInitialRenderer();"));
assert!(html.contains("__MNOTE_TREE_SHELL_OVERRIDE__"));
}
@@ -4442,6 +4548,9 @@ mod tests {
assert!(filetree_html.contains("\"filetreeSelection\""));
assert!(filetree_html.contains("\"selectedRowIds\""));
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\"]"));
let picker_response = app()
.oneshot(
@@ -4460,6 +4569,8 @@ mod tests {
assert!(picker_html.contains("\"mode\":\"picker\""));
assert!(picker_html.contains("\"activePickerItem\":\"page_child\""));
assert!(picker_html.contains("\"excludedPickerIds\":[\"page_root\"]"));
assert!(picker_html.contains("\"runtimeArtifact\""));
assert!(picker_html.contains("\"contractName\":\"rust_tree_shell_runtime_artifact_v1\""));
}
#[tokio::test]
@@ -3,12 +3,15 @@ pub mod drag_drop_state;
pub mod dispatcher;
pub mod expansion_state;
pub mod filetree_renderer;
pub mod filetree_runtime;
pub mod filetree_selection;
pub mod focus_state;
pub mod keyboard_state;
pub mod loader;
pub mod page_renderer;
pub mod page_runtime;
pub mod picker_renderer;
pub mod picker_runtime;
pub mod picker_state;
pub mod protocol;
pub mod renderer_input;
@@ -78,8 +78,19 @@ fn render_page_row(
.find(|source| source.node_id == row.node_id)
.map(|source| source.expanded)
.unwrap_or(false);
let toggle_html = if row.expandable {
format!(
r#"<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="{node_id}" aria-label="{label} {title}">{marker}</button>"#,
node_id = escape_html(&row.node_id),
label = if expanded { "折叠" } else { "展开" },
title = escape_html(&row.title),
marker = if expanded { "" } else { "" },
)
} else {
r#"<span class="tree-spacer" aria-hidden="true"></span>"#.to_string()
};
html.push_str(&format!(
r#"<li class="tree-node" data-node-id="{node_id}"><div class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="page" data-testid="{test_id}" data-node-id="{node_id}" data-shell-mode="page" data-active="{active}" data-focused="{focused}" data-draggable="true" draggable="true" tabindex="{tab_index}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="{node_id}"><span class="tree-link-title">{title}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="{node_id}" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="{node_id}" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="{node_id}" aria-label="更多操作">…</button></div></div>"#,
r#"<li class="tree-node" data-node-id="{node_id}"><div class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="page" data-testid="{test_id}" data-node-id="{node_id}" data-shell-mode="page" data-active="{active}" data-focused="{focused}" data-draggable="true" draggable="true" tabindex="{tab_index}">{toggle_html}<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="{node_id}"><span class="tree-link-title">{title}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="{node_id}" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="{node_id}" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="{node_id}" aria-label="更多操作">…</button></div></div>"#,
node_id = escape_html(&row.node_id),
aria_level = row.depth + 1,
expanded_attr = if row.expandable && expanded { "true" } else { "false" },
@@ -87,6 +98,7 @@ fn render_page_row(
active = active,
focused = focused,
tab_index = if focused { "0" } else { "-1" },
toggle_html = toggle_html,
title = escape_html(&row.title),
));
if row.expandable && expanded {
@@ -206,6 +218,8 @@ mod tests {
assert!(html.contains("data-rust-rendered-row=\"page\""));
assert!(html.contains("data-shell-mode=\"page\""));
assert!(html.contains("data-rust-action=\"open\""));
assert!(html.contains("data-rust-action=\"toggle\""));
assert!(html.contains("data-testid=\"tree-node-toggle\""));
assert!(html.contains("data-rust-action=\"create\""));
assert!(html.contains("draggable=\"true\""));
assert!(html.contains("tree-children"));
@@ -52,11 +52,12 @@ fn render_picker_row(
children_by_parent: &BTreeMap<Option<String>, Vec<PickerRenderRow>>,
) {
html.push_str(&format!(
r#"<li class="tree-node" data-node-id="{node_id}"><button type="button" class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="picker" data-testid="tree-picker-row" data-node-id="{node_id}" data-shell-mode="picker" data-focused="{active}" data-rust-action="pick"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">{title}</span></span></button>"#,
r#"<li class="tree-node" data-node-id="{node_id}"><button type="button" class="tree-row" role="treeitem" aria-level="{aria_level}" aria-expanded="{expanded_attr}" data-rust-rendered-row="picker" data-testid="tree-picker-row" data-node-id="{node_id}" data-shell-mode="picker" data-focused="{active}" data-rust-action="pick" tabindex="{tabindex}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">{title}</span></span></button>"#,
node_id = escape_html(&row.node_id),
aria_level = row.depth + 1,
expanded_attr = if row.expandable && row.expanded { "true" } else { "false" },
active = row.active,
tabindex = if row.active { "0" } else { "-1" },
title = escape_html(&row.title),
));
if row.expandable && row.expanded {
@@ -77,8 +78,9 @@ pub fn render_initial_picker_html(input: &PickerInitialRenderInput) -> String {
);
if input.allow_root_pick {
html.push_str(&format!(
r#"<li class="tree-node" data-node-id="__root__"><button type="button" class="tree-row" data-testid="tree-picker-root" data-rust-rendered-row="picker-root" data-rust-action="pick-root" data-focused="{focused}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">根目录</span></span></button></li>"#,
r#"<li class="tree-node" data-node-id="__root__"><button type="button" class="tree-row" data-testid="tree-picker-root" data-rust-rendered-row="picker-root" data-rust-action="pick-root" data-focused="{focused}" tabindex="{tabindex}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">根目录</span></span></button></li>"#,
focused = input.root_active,
tabindex = if input.root_active { "0" } else { "-1" },
));
}
@@ -180,6 +182,14 @@ mod tests {
expandable: false,
expanded: false,
active: true,
}, PickerRenderRow {
node_id: "page_other".into(),
parent_node_id: None,
title: "其他页面".into(),
depth: 0,
expandable: false,
expanded: false,
active: false,
}],
});
@@ -190,5 +200,7 @@ mod tests {
assert!(html.contains("data-testid=\"tree-picker-row\""));
assert!(html.contains("首页 &lt;安全&gt;"));
assert!(html.contains("data-focused=\"true\""));
assert!(html.contains("tabindex=\"0\""));
assert!(html.contains("tabindex=\"-1\""));
}
}
@@ -26,6 +26,7 @@ pub struct TreeShellRendererInput {
pub excluded_picker_ids: BTreeSet<String>,
pub picker_state_reducer: Option<PickerStateReducerContract>,
pub command_dispatcher: TreeShellCommandDispatcher,
pub runtime_artifact: TreeShellRuntimeArtifactBoundary,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
@@ -35,6 +36,41 @@ pub struct TreeShellCommandDispatcher {
pub command_names: BTreeSet<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TreeShellRuntimeArtifactBoundary {
pub contract_name: &'static str,
pub input_fields: BTreeSet<&'static str>,
pub output_channels: BTreeSet<&'static str>,
pub event_kinds: BTreeSet<&'static str>,
}
impl Default for TreeShellRuntimeArtifactBoundary {
fn default() -> Self {
Self {
contract_name: "rust_tree_shell_runtime_artifact_v1",
input_fields: BTreeSet::from([
"rendererInput",
"projectionItems",
"expandedIds",
"selectedRowIds",
"activePickerItem",
"focusedId",
]),
output_channels: BTreeSet::from(["domPatch", "intentEvent", "commandDispatchEvent"]),
event_kinds: BTreeSet::from([
"focus",
"keyboard",
"expandCollapse",
"selection",
"contextMenu",
"dragDrop",
"pick",
]),
}
}
}
impl TreeShellRendererInput {
pub fn page(input: PageTreeRendererInput) -> Self {
Self {
@@ -49,6 +85,7 @@ impl TreeShellRendererInput {
excluded_picker_ids: BTreeSet::new(),
picker_state_reducer: None,
command_dispatcher: input.command_dispatcher,
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
}
}
@@ -65,6 +102,7 @@ impl TreeShellRendererInput {
excluded_picker_ids: BTreeSet::new(),
picker_state_reducer: None,
command_dispatcher: input.command_dispatcher,
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
}
}
@@ -81,6 +119,7 @@ impl TreeShellRendererInput {
excluded_picker_ids: input.excluded_picker_ids,
picker_state_reducer: Some(PickerStateReducerContract::default()),
command_dispatcher: input.command_dispatcher,
runtime_artifact: TreeShellRuntimeArtifactBoundary::default(),
}
}
}
@@ -113,8 +152,8 @@ pub struct PickerRendererInput {
#[cfg(test)]
mod tests {
use super::{
FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput, TreeShellCommandDispatcher,
TreeShellRendererInput, TreeShellRendererMode,
FileTreeRendererInput, PageTreeRendererInput, PickerRendererInput,
TreeShellCommandDispatcher, TreeShellRendererInput, TreeShellRendererMode,
};
use crate::tree_shell::filetree_selection::FileTreeSelectionState;
use std::collections::BTreeSet;
@@ -158,7 +197,10 @@ mod tests {
});
assert_eq!(filetree.mode, TreeShellRendererMode::FileTree);
assert_eq!(filetree.focused_id.as_deref(), Some("asset:a"));
assert!(filetree.filetree_selection.selected_row_ids.contains("asset:a"));
assert!(filetree
.filetree_selection
.selected_row_ids
.contains("asset:a"));
assert!(filetree.page_focus_keyboard_reducer.is_none());
assert_eq!(
filetree
@@ -188,4 +230,36 @@ mod tests {
Some("rust_picker_state_reducer_v1")
);
}
#[test]
fn tree_shell_renderer_input_exposes_runtime_artifact_boundary() {
let page = TreeShellRendererInput::page(PageTreeRendererInput {
projection_item_ids: vec!["doc:root".into()],
expanded_ids: set(&["doc:root"]),
focused_id: Some("doc:root".into()),
command_dispatcher: dispatcher(),
});
let artifact = page.runtime_artifact;
assert_eq!(
artifact.contract_name,
"rust_tree_shell_runtime_artifact_v1"
);
assert!(artifact.input_fields.contains("rendererInput"));
assert!(artifact.input_fields.contains("projectionItems"));
assert!(artifact.input_fields.contains("expandedIds"));
assert!(artifact.input_fields.contains("selectedRowIds"));
assert!(artifact.input_fields.contains("activePickerItem"));
assert!(artifact.input_fields.contains("focusedId"));
assert!(artifact.output_channels.contains("domPatch"));
assert!(artifact.output_channels.contains("intentEvent"));
assert!(artifact.output_channels.contains("commandDispatchEvent"));
assert!(artifact.event_kinds.contains("focus"));
assert!(artifact.event_kinds.contains("keyboard"));
assert!(artifact.event_kinds.contains("expandCollapse"));
assert!(artifact.event_kinds.contains("selection"));
assert!(artifact.event_kinds.contains("contextMenu"));
assert!(artifact.event_kinds.contains("dragDrop"));
assert!(artifact.event_kinds.contains("pick"));
}
}