diff --git a/api/cpp/include/slint-testing.h b/api/cpp/include/slint-testing.h index 9188e5e4d2..7568bd9825 100644 --- a/api/cpp/include/slint-testing.h +++ b/api/cpp/include/slint-testing.h @@ -250,6 +250,12 @@ public: cbindgen_private::AccessibleStringProperty::Description); } + /// Returns the accessible-id of that element, if any. + std::optional accessible_id() const + { + return get_accessible_string_property(cbindgen_private::AccessibleStringProperty::Id); + } + /// Returns the accessible-value-maximum of that element, if any. std::optional accessible_value_maximum() const { diff --git a/docs/astro/src/content/docs/reference/common.mdx b/docs/astro/src/content/docs/reference/common.mdx index fab5e3f8d1..2ec5ce3194 100644 --- a/docs/astro/src/content/docs/reference/common.mdx +++ b/docs/astro/src/content/docs/reference/common.mdx @@ -412,6 +412,23 @@ Whether the element is expanded or not. Applies to combo boxes, menu items, tree view items and other widgets. +### accessible-id + +A unique identifier for the element, used to identify widgets for automation and testing purposes. +This property can be set to any string value and is particularly useful when writing automated tests or +when using accessibility tools to uniquely identify specific widgets in your application. + +If you need to identify repeated elements, make sure to assign different values to each instance as illustrated below. + +```slint +for i in 5: Rectangle { + accessible-role: button; + accessible-id: "btn-" + i; + accessible-label: "Button " + i; +} +``` + + ### accessible-label The label for an interactive element. (default value: empty for most elements, or the value of the `text` property for Text elements) diff --git a/internal/backends/testing/search_api.rs b/internal/backends/testing/search_api.rs index ee0fd67fc4..3066defc86 100644 --- a/internal/backends/testing/search_api.rs +++ b/internal/backends/testing/search_api.rs @@ -624,6 +624,16 @@ impl ElementHandle { .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Description)) } + /// Returns the value of the `accessible-id` property, if present + pub fn accessible_id(&self) -> Option { + if self.element_index != 0 { + return None; + } + self.item + .upgrade() + .and_then(|item| item.accessible_string_property(AccessibleStringProperty::Id)) + } + /// Returns the value of the `accessible-checked` property, if present pub fn accessible_checked(&self) -> Option { if self.element_index != 0 { diff --git a/internal/backends/winit/accesskit.rs b/internal/backends/winit/accesskit.rs index 5bceec2036..a255873f75 100644 --- a/internal/backends/winit/accesskit.rs +++ b/internal/backends/winit/accesskit.rs @@ -577,6 +577,10 @@ impl NodeCollection { node.set_description(description.to_string()); } + if let Some(id) = item.accessible_string_property(AccessibleStringProperty::Id) { + node.set_author_id(id.to_string()); + } + if item .accessible_string_property(AccessibleStringProperty::Expandable) .is_some_and(|x| x == "true") diff --git a/internal/compiler/typeregister.rs b/internal/compiler/typeregister.rs index 1a46d8792a..85a10d4c12 100644 --- a/internal/compiler/typeregister.rs +++ b/internal/compiler/typeregister.rs @@ -207,6 +207,7 @@ pub fn reserved_accessibility_properties() -> impl Iterator +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Test the accessible-id property for uniquely identifying widgets for automation + +export component TestCase inherits Rectangle { + width: 300phx; + height: 300phx; + + VerticalLayout { + button1 := Rectangle { + accessible-role: button; + accessible-label: "Click Me"; + accessible-id: "my-unique-button"; + } + + button2 := Rectangle { + accessible-role: button; + accessible-label: "Another Button"; + } + } +} + + +/* + +```rust +let instance = TestCase::new().unwrap(); + +let buttons = slint_testing::ElementHandle::find_by_accessible_label(&instance, "Click Me").collect::>(); +assert_eq!(buttons.len(), 1); +assert_eq!(buttons[0].accessible_id(), Some("my-unique-button".into())); + +let buttons = slint_testing::ElementHandle::find_by_accessible_label(&instance, "Another Button").collect::>(); +assert_eq!(buttons.len(), 1); +assert!(buttons[0].accessible_id().is_none()); +``` + +```cpp +auto handle = TestCase::create(); + +auto button_search = slint::testing::ElementHandle::find_by_accessible_label(handle, "Click Me"); +assert(button_search.size() == 1); +auto button_with_id = button_search[0]; +auto accessible_id = button_with_id.accessible_id(); +assert(accessible_id.has_value()); +assert(*accessible_id == "my-unique-button"); + +button_search = slint::testing::ElementHandle::find_by_accessible_label(handle, "Another Button"); +assert(button_search.size() == 1); +auto button_without_id = button_search[0]; +accessible_id = button_without_id.accessible_id(); +assert(!accessible_id.has_value()); +``` + +*/