openpencil/crates/op-host-desktop/src/image_search_session_tests.rs
Fini 6c930c8eaa
Some checks failed
Rust check (native) / macos-latest / 1.94 (push) Waiting to run
Rust check (native) / windows-latest / 1.94 (push) Waiting to run
Rust multi-platform build / linux-aarch64 (push) Waiting to run
Rust multi-platform build / macos-aarch64 (push) Waiting to run
Rust multi-platform build / windows-x86_64 (push) Waiting to run
Rust multi-platform build / macos-x86_64 (push) Waiting to run
Rust multi-platform build / windows-aarch64 (push) Waiting to run
Rust multi-platform build / ios-aarch64 (cargo check only) (push) Waiting to run
Rust multi-platform build / ios-aarch64-sim (cargo check only) (push) Waiting to run
Rust check (native) / ubuntu-latest / 1.94 (push) Failing after 2s
Rust check (native) / cargo-deny (native) (push) Failing after 2s
Rust check (native) / diagnostics golden drift (push) Failing after 2s
Rust multi-platform build / linux-x86_64 (push) Failing after 4s
Rust multi-platform build / wasm32-unknown-unknown / op-host-web (compile guard) (push) Failing after 1s
Rust multi-platform build / android-aarch64 (cargo check only) (push) Failing after 2s
Rust multi-platform build / android-x86_64 (cargo check only) (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo check --target wasm32-unknown-unknown (push) Failing after 1s
WASM bundle check (kickoff §1.2) / cargo-deny --target wasm32-unknown-unknown check bans (push) Failing after 1s
test(ai): cover image search job polling
2026-05-30 19:24:27 +08:00

284 lines
8.4 KiB
Rust

use super::*;
use jian_ops_schema::node::base::PenNodeBase;
use jian_ops_schema::node::{ContainerProps, FrameNode, ImageNode, PenNode, TextContent, TextNode};
use jian_ops_schema::sizing::SizingBehavior;
use jian_ops_schema::style::{ImageFillMode, PenFill, SolidFillBody};
fn image_node(id: &str, src: &str, query: Option<&str>) -> PenNode {
PenNode::Image(ImageNode {
base: PenNodeBase {
id: id.to_string(),
name: Some("Menu photo".into()),
..Default::default()
},
src: src.to_string(),
object_fit: None,
width: Some(SizingBehavior::Number(240.0)),
height: Some(SizingBehavior::Number(160.0)),
corner_radius: None,
effects: None,
exposure: None,
contrast: None,
saturation: None,
temperature: None,
tint: None,
highlights: None,
shadows: None,
image_prompt: None,
image_search_query: query.map(str::to_string),
state: None,
bindings: None,
events: None,
lifecycle: None,
semantics: None,
gestures: None,
route: None,
})
}
fn text_label(id: &str, role: Option<&str>, content: &str) -> PenNode {
PenNode::Text(TextNode {
base: PenNodeBase {
id: id.to_string(),
name: Some("Label".into()),
role: role.map(str::to_string),
..Default::default()
},
width: Some(SizingBehavior::Number(160.0)),
height: Some(SizingBehavior::Number(24.0)),
content: TextContent::Plain(content.to_string()),
font_family: None,
font_size: None,
font_weight: None,
font_style: None,
letter_spacing: None,
line_height: None,
text_align: None,
text_align_vertical: None,
text_growth: None,
underline: None,
strikethrough: None,
fill: None,
effects: None,
state: None,
bindings: None,
events: None,
lifecycle: None,
semantics: None,
gestures: None,
route: None,
})
}
fn frame_node(
id: &str,
name: &str,
role: Option<&str>,
fill: Option<Vec<PenFill>>,
children: Vec<PenNode>,
) -> PenNode {
PenNode::Frame(FrameNode {
base: PenNodeBase {
id: id.to_string(),
name: Some(name.into()),
role: role.map(str::to_string),
..Default::default()
},
container: ContainerProps {
width: Some(SizingBehavior::Number(240.0)),
height: Some(SizingBehavior::Number(160.0)),
fill,
..Default::default()
},
children: Some(children),
reusable: None,
slot: None,
state: None,
bindings: None,
events: None,
lifecycle: None,
semantics: None,
gestures: None,
route: None,
})
}
fn solid_fill() -> PenFill {
PenFill::Solid(SolidFillBody {
color: "#E5E7EB".into(),
explain: None,
opacity: None,
blend_mode: None,
})
}
#[test]
fn collect_targets_prefers_query_on_empty_image_nodes() {
let mut state = EditorState::default();
state.active_children_mut().clear();
state
.active_children_mut()
.push(image_node("img1", "", Some("burger fries")));
let targets = collect_targets(&state, &HashSet::new());
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].node_id.as_str(), "img1");
assert_eq!(targets[0].query, "burger fries");
}
#[test]
fn apply_result_sets_empty_image_src() {
let mut state = EditorState::default();
state.active_children_mut().clear();
state
.active_children_mut()
.push(image_node("img1", "", Some("burger fries")));
assert!(apply_result(
&mut state,
&NodeId::new("img1"),
"https://example.com/photo.jpg"
));
let PenNode::Image(image) = &state.active_children()[0] else {
panic!("expected image");
};
assert_eq!(image.src, "https://example.com/photo.jpg");
}
#[test]
fn collect_targets_includes_unfilled_placeholder_frames() {
let mut state = EditorState::default();
state.active_children_mut().clear();
state.active_children_mut().push(frame_node(
"photo",
"Image",
Some("image-placeholder"),
Some(vec![solid_fill()]),
vec![text_label(
"label",
Some("image-placeholder-label"),
"pizza hero",
)],
));
let targets = collect_targets(&state, &HashSet::new());
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].node_id.as_str(), "photo");
assert_eq!(targets[0].query, "pizza hero");
}
#[test]
fn collect_targets_uses_parent_semantic_name_for_generic_heuristic_frame() {
let mut state = EditorState::default();
state.active_children_mut().clear();
let photo = frame_node("photo", "Image", None, Some(vec![solid_fill()]), Vec::new());
state
.active_children_mut()
.push(frame_node("card", "Bella Italia", None, None, vec![photo]));
let targets = collect_targets(&state, &HashSet::new());
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].node_id.as_str(), "photo");
assert_eq!(targets[0].query, "Bella Italia");
}
#[test]
fn apply_result_repaints_placeholder_frame_with_image_fill_and_clears_children() {
let mut state = EditorState::default();
state.active_children_mut().clear();
state.active_children_mut().push(frame_node(
"photo",
"Image",
Some("image-placeholder"),
Some(vec![solid_fill()]),
vec![text_label(
"label",
Some("image-placeholder-label"),
"pizza hero",
)],
));
assert!(apply_result(
&mut state,
&NodeId::new("photo"),
"https://example.com/photo.jpg"
));
let PenNode::Frame(frame) = &state.active_children()[0] else {
panic!("expected frame");
};
let Some([PenFill::Image(image_fill)]) = frame.container.fill.as_deref() else {
panic!("expected single image fill");
};
assert_eq!(image_fill.url, "https://example.com/photo.jpg");
assert_eq!(image_fill.mode, Some(ImageFillMode::Crop));
assert_eq!(frame.children.as_deref(), Some(&[][..]));
}
#[test]
fn poll_into_applies_finished_job_to_placeholder_frame() {
let mut state = EditorState::default();
state.active_children_mut().clear();
state.active_children_mut().push(frame_node(
"photo",
"Image",
Some("image-placeholder"),
Some(vec![solid_fill()]),
vec![text_label(
"label",
Some("image-placeholder-label"),
"pizza hero",
)],
));
let (tx, rx) = std::sync::mpsc::channel();
tx.send(Some("https://example.com/photo.jpg".to_string()))
.unwrap();
let mut session = ImageSearchSession {
in_flight: HashSet::from(["photo".to_string()]),
completed: HashSet::new(),
jobs: vec![ImageSearchJob {
node_id: NodeId::new("photo"),
rx,
}],
};
assert!(session.poll_into(&mut state));
assert!(!session.is_pending());
assert!(session.in_flight.is_empty());
assert!(session.completed.contains("photo"));
let PenNode::Frame(frame) = &state.active_children()[0] else {
panic!("expected frame");
};
let Some([PenFill::Image(image_fill)]) = frame.container.fill.as_deref() else {
panic!("expected single image fill");
};
assert_eq!(image_fill.url, "https://example.com/photo.jpg");
assert_eq!(frame.children.as_deref(), Some(&[][..]));
}
#[test]
fn openverse_credentials_require_both_fields() {
let mut state = EditorState::default();
assert!(OpenverseCredentials::from_state(&state).is_none());
state.editor_ui.agent_settings.openverse_client_id = " client ".into();
assert!(OpenverseCredentials::from_state(&state).is_none());
state.editor_ui.agent_settings.openverse_client_secret = " secret ".into();
let credentials = OpenverseCredentials::from_state(&state).expect("complete credentials");
assert_eq!(credentials.client_id, "client");
assert_eq!(credentials.client_secret, "secret");
}
#[tokio::test]
#[ignore = "network smoke test for Openverse/Wikimedia"]
async fn fetch_first_image_url_smoke() {
let url = fetch_first_image_url("burger fries", None)
.await
.expect("common query should return an image URL");
assert!(url.starts_with("http"), "got {url}");
}