use super::protocol; use std::collections::{BTreeMap, BTreeSet}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct PickerRenderRow { pub node_id: String, pub parent_node_id: Option, pub title: String, pub depth: u32, pub expandable: bool, pub expanded: bool, pub active: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PickerRenderResult { pub root_test_id: &'static str, pub item_count: usize, pub allow_root_pick: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PickerInitialRenderInput { pub rows: Vec, pub allow_root_pick: bool, pub root_active: bool, } pub fn build_picker_render_result( rows: &[PickerRenderRow], allow_root_pick: bool, ) -> PickerRenderResult { PickerRenderResult { root_test_id: protocol::TEST_ID_TREE_PICKER_ROOT, item_count: rows.len(), allow_root_pick, } } fn escape_html(input: &str) -> String { input .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } fn render_picker_row( html: &mut String, row: &PickerRenderRow, children_by_parent: &BTreeMap, Vec>, ) { html.push_str(&format!( r#"
  • "#, 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 { if let Some(children) = children_by_parent.get(&Some(row.node_id.clone())) { html.push_str(r#"
      "#); for child in children { render_picker_row(html, child, children_by_parent); } html.push_str("
    "); } } html.push_str("
  • "); } pub fn render_initial_picker_html(input: &PickerInitialRenderInput) -> String { let mut html = String::from( r#"
      "#, ); if input.allow_root_pick { html.push_str(&format!( r#"
    • "#, focused = input.root_active, tabindex = if input.root_active { "0" } else { "-1" }, )); } let ids = input .rows .iter() .map(|row| row.node_id.clone()) .collect::>(); let mut children_by_parent = BTreeMap::, Vec>::new(); for row in &input.rows { let parent_id = row .parent_node_id .as_ref() .filter(|parent_id| ids.contains(*parent_id)) .cloned(); children_by_parent .entry(parent_id) .or_default() .push(row.clone()); } if let Some(roots) = children_by_parent.get(&None).cloned() { for root in &roots { render_picker_row(&mut html, root, &children_by_parent); } } html.push_str("
    "); html } #[cfg(test)] mod tests { use super::super::filetree_renderer::{build_filetree_testids, FileTreeRenderRow}; use super::{ build_picker_render_result, render_initial_picker_html, PickerInitialRenderInput, PickerRenderRow, }; #[test] fn tree_shell_filetree_picker_builds_file_rows_and_picker_mode() { let file_rows = build_filetree_testids(&[ FileTreeRenderRow { row_id: "doc:page_root".into(), row_kind: "document".into(), node_id: "page_root".into(), parent_node_id: None, title: "首页".into(), depth: 0, expandable: false, expanded: false, icon_kind: "page".into(), document_id: Some("page_root".into()), asset_id: None, object_identity: None, selected: false, }, FileTreeRenderRow { row_id: "asset:asset_1".into(), row_kind: "asset".into(), node_id: "asset:asset_1".into(), parent_node_id: Some("page_root".into()), title: "附件".into(), depth: 1, expandable: false, expanded: false, icon_kind: "file".into(), document_id: Some("page_root".into()), asset_id: Some("asset_1".into()), object_identity: None, selected: false, }, ]); let picker = build_picker_render_result( &[PickerRenderRow { node_id: "page_root".into(), parent_node_id: None, title: "首页".into(), depth: 0, expandable: false, expanded: false, active: false, }], true, ); assert_eq!(file_rows[0].0, "filetree-doc-row"); assert_eq!(file_rows[1].0, "filetree-asset-row"); assert_eq!(picker.root_test_id, "tree-picker-root"); assert!(picker.allow_root_pick); } #[test] fn tree_shell_picker_renderer_outputs_initial_html_contract() { let html = render_initial_picker_html(&PickerInitialRenderInput { allow_root_pick: true, root_active: false, rows: vec![ PickerRenderRow { node_id: "page_root".into(), parent_node_id: None, title: "首页 <安全>".into(), depth: 0, 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, }, ], }); assert!(html.contains("data-rust-picker-renderer=\"initial_v1\"")); assert!(html.contains("data-rust-rendered-row=\"picker-root\"")); assert!(html.contains("data-rust-rendered-row=\"picker\"")); assert!(html.contains("data-testid=\"tree-picker-root\"")); assert!(html.contains("data-testid=\"tree-picker-row\"")); assert!(html.contains("首页 <安全>")); assert!(html.contains("data-focused=\"true\"")); assert!(html.contains("tabindex=\"0\"")); assert!(html.contains("tabindex=\"-1\"")); } }