Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion crates/originweave-core/src/semantic_action_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ pub struct SemanticNodeActionTarget {
}

impl SemanticNodeActionTarget {
/// Construct a target only when the observation advertised the requested node-local action.
/// Construct a target only when the observation advertises a currently coherent node action.
pub fn from_observation(
observation: &SemanticNodeObservation,
action: NodeActionKind,
) -> Result<Self, SemanticNodeActionTargetError> {
if !observation.supported_actions().contains(&action) {
return Err(SemanticNodeActionTargetError::UnsupportedAction);
}
if action != NodeActionKind::ScrollIntoView && !observation.is_enabled() {
return Err(SemanticNodeActionTargetError::NodeNotEnabled);
}
Ok(Self {
handle: observation.handle().clone(),
action,
Expand Down Expand Up @@ -64,6 +67,8 @@ impl SemanticNodeActionTarget {
pub enum SemanticNodeActionTargetError {
/// The requested action was not advertised by the semantic observation.
UnsupportedAction,
/// The observation reported the target disabled for an interactive action.
NodeNotEnabled,
}

impl fmt::Display for SemanticNodeActionTargetError {
Expand All @@ -72,6 +77,9 @@ impl fmt::Display for SemanticNodeActionTargetError {
Self::UnsupportedAction => {
formatter.write_str("semantic node action is not advertised by the observation")
}
Self::NodeNotEnabled => {
formatter.write_str("semantic node is not enabled for the requested action")
}
}
}
}
Expand Down
32 changes: 30 additions & 2 deletions crates/originweave-core/tests/semantic_node_action_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ use originweave_core::{
};

fn observation() -> Result<SemanticNodeObservation, String> {
observation_with_enabled(true)
}

fn observation_with_enabled(enabled: bool) -> Result<SemanticNodeObservation, String> {
let handle = ObservedNodeHandle::new(
BrowserSessionId::new(7).map_err(|error| error.to_string())?,
BrowsingContextId::new(11).map_err(|error| error.to_string())?,
Expand All @@ -23,10 +27,10 @@ fn observation() -> Result<SemanticNodeObservation, String> {
role: "button".to_owned(),
accessible_name: "Save draft".to_owned(),
visible_text: Some("Save draft".to_owned()),
enabled: true,
enabled,
visible: true,
selected: None,
supported_actions: BTreeSet::from([NodeActionKind::Click]),
supported_actions: BTreeSet::from([NodeActionKind::Click, NodeActionKind::ScrollIntoView]),
evidence_channels: BTreeSet::from([ObservationChannel::Accessibility]),
})
.map_err(|error| error.to_string())
Expand All @@ -53,6 +57,26 @@ fn unsupported_node_action_fails_closed_without_minting_authority() -> Result<()
Ok(())
}

#[test]
fn disabled_interactive_node_action_fails_closed() -> Result<(), String> {
let observed = observation_with_enabled(false)?;
assert_eq!(
SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click).err(),
Some(SemanticNodeActionTargetError::NodeNotEnabled)
);
Ok(())
}

#[test]
fn disabled_node_can_still_be_targeted_for_scroll_only() -> Result<(), String> {
let observed = observation_with_enabled(false)?;
let target =
SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::ScrollIntoView)
.map_err(|error| error.to_string())?;
assert_eq!(target.action(), NodeActionKind::ScrollIntoView);
Ok(())
}

#[test]
fn node_action_target_revalidates_exact_browser_authority() -> Result<(), String> {
let observed = observation()?;
Expand Down Expand Up @@ -181,4 +205,8 @@ fn node_action_target_error_is_stable_and_credential_free() {
SemanticNodeActionTargetError::UnsupportedAction.to_string(),
"semantic node action is not advertised by the observation"
);
assert_eq!(
SemanticNodeActionTargetError::NodeNotEnabled.to_string(),
"semantic node is not enabled for the requested action"
);
}
Loading