C++ testing API: Intreoduce the ElementHandle

This commit is contained in:
Olivier Goffart 2024-04-19 18:18:35 +02:00
parent 13fe59cc2e
commit 475ced0a62
15 changed files with 225 additions and 25 deletions

View file

@ -342,7 +342,7 @@ jobs:
with: with:
crate: cross crate: cross
- name: Check - name: Check
run: cross check --target=armv7-unknown-linux-gnueabihf -p slint-cpp --no-default-features --features=internal-testing,interpreter,std run: cross check --target=armv7-unknown-linux-gnueabihf -p slint-cpp --no-default-features --features=testing,interpreter,std
uefi-demo: uefi-demo:
env: env:

View file

@ -104,6 +104,7 @@ define_cargo_dependent_feature(backend-linuxkms-noseat "Enable support for the b
define_cargo_dependent_feature(gettext "Enable support of translations using gettext" OFF "NOT SLINT_FEATURE_FREESTANDING") define_cargo_dependent_feature(gettext "Enable support of translations using gettext" OFF "NOT SLINT_FEATURE_FREESTANDING")
define_cargo_dependent_feature(accessibility "Enable integration with operating system provided accessibility APIs" ON "NOT SLINT_FEATURE_FREESTANDING") define_cargo_dependent_feature(accessibility "Enable integration with operating system provided accessibility APIs" ON "NOT SLINT_FEATURE_FREESTANDING")
define_cargo_dependent_feature(testing "Enable support for testing API (experimental)" ON "NOT SLINT_FEATURE_FREESTANDING")
define_cargo_feature(experimental "Enable experimental features. (No backward compatibility guarantees)" OFF) define_cargo_feature(experimental "Enable experimental features. (No backward compatibility guarantees)" OFF)
if (SLINT_BUILD_RUNTIME) if (SLINT_BUILD_RUNTIME)

View file

@ -27,7 +27,7 @@ name = "slint_cpp"
[features] [features]
interpreter = ["slint-interpreter", "std"] interpreter = ["slint-interpreter", "std"]
# Enable some function used by the integration tests # Enable some function used by the integration tests
internal-testing = ["i-slint-backend-testing"] testing = ["dep:i-slint-backend-testing"]
backend-qt = ["i-slint-backend-selector/backend-qt", "std"] backend-qt = ["i-slint-backend-selector/backend-qt", "std"]
backend-winit = ["i-slint-backend-selector/backend-winit", "std"] backend-winit = ["i-slint-backend-selector/backend-winit", "std"]
@ -51,7 +51,7 @@ default = ["std", "backend-winit", "renderer-femtovg", "backend-qt"]
[dependencies] [dependencies]
i-slint-backend-selector = { workspace = true, optional = true } i-slint-backend-selector = { workspace = true, optional = true }
i-slint-backend-testing = { workspace = true, optional = true } i-slint-backend-testing = { workspace = true, optional = true, features = ["ffi"] }
i-slint-renderer-skia = { workspace = true, features = ["default", "x11", "wayland"], optional = true } i-slint-renderer-skia = { workspace = true, features = ["default", "x11", "wayland"], optional = true }
i-slint-core = { workspace = true, features = ["ffi"] } i-slint-core = { workspace = true, features = ["ffi"] }
slint-interpreter = { workspace = true, features = ["ffi", "compat-1-2"], optional = true } slint-interpreter = { workspace = true, features = ["ffi", "compat-1-2"], optional = true }

View file

@ -805,6 +805,29 @@ fn gen_backend_qt(
Ok(()) Ok(())
} }
fn gen_testing(
root_dir: &Path,
include_dir: &Path,
dependencies: &mut Vec<PathBuf>,
) -> anyhow::Result<()> {
let config = default_config();
let mut crate_dir = root_dir.to_owned();
crate_dir.extend(["internal", "backends", "testing"].iter());
ensure_cargo_rerun_for_crate(&crate_dir, dependencies)?;
cbindgen::Builder::new()
.with_config(config)
.with_crate(crate_dir)
.with_include("slint_testing_internal.h")
.generate()
.context("Unable to generate bindings for slint_testing_internal.h")?
.write_to_file(include_dir.join("slint_testing_internal.h"));
Ok(())
}
fn gen_platform( fn gen_platform(
root_dir: &Path, root_dir: &Path,
include_dir: &Path, include_dir: &Path,
@ -933,7 +956,7 @@ macro_rules! declare_features {
}; };
} }
declare_features! {interpreter backend_qt freestanding renderer_software renderer_skia experimental gettext} declare_features! {interpreter backend_qt freestanding renderer_software renderer_skia experimental gettext testing}
/// Generate the headers. /// Generate the headers.
/// `root_dir` is the root directory of the slint git repo /// `root_dir` is the root directory of the slint git repo
@ -953,6 +976,9 @@ pub fn gen_all(
gen_corelib(root_dir, include_dir, &mut deps, enabled_features)?; gen_corelib(root_dir, include_dir, &mut deps, enabled_features)?;
gen_backend_qt(root_dir, include_dir, &mut deps)?; gen_backend_qt(root_dir, include_dir, &mut deps)?;
gen_platform(root_dir, include_dir, &mut deps)?; gen_platform(root_dir, include_dir, &mut deps)?;
if enabled_features.testing {
gen_testing(root_dir, include_dir, &mut deps)?;
}
if enabled_features.interpreter { if enabled_features.interpreter {
gen_interpreter(root_dir, include_dir, &mut deps)?; gen_interpreter(root_dir, include_dir, &mut deps)?;
} }

View file

@ -0,0 +1,130 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial
#include "slint.h"
#include "slint_testing_internal.h"
#include <optional>
#include <string_view>
#ifdef SLINT_FEATURE_TESTING
namespace slint::testing {
/// Init the testing backend.
/// Should be called before any other Slint function that can access the platform.
/// Then future windows will not appear on the screen anymore
inline void init()
{
cbindgen_private::slint_testing_init_backend();
}
/// A Handle to an element to query accessible property for testing purposes.
///
/// Use find_by_accessible_label() to obtain all elements matching the given accessible label.
class ElementHandle
{
cbindgen_private::ItemRc inner;
public:
/// Find all elements matching the given accessible label.
template<typename T>
static SharedVector<ElementHandle> find_by_accessible_label(const ComponentHandle<T> &component,
std::string_view label)
{
cbindgen_private::Slice<uint8_t> label_view {
const_cast<unsigned char *>(reinterpret_cast<const unsigned char *>(label.data())),
label.size()
};
auto vrc = component.into_dyn();
SharedVector<ElementHandle> result;
cbindgen_private::slint_testing_element_find_by_accessible_label(
&vrc, &label_view,
reinterpret_cast<SharedVector<cbindgen_private::ItemRc> *>(&result));
return result;
}
/// Returns the accessible-label of that element, if any.
std::optional<SharedString> accessible_label() const
{
SharedString result;
if (inner.item_tree.vtable()->accessible_string_property(
inner.item_tree.borrow(), inner.index,
cbindgen_private::AccessibleStringProperty::Label, &result)) {
return result;
} else {
return std::nullopt;
}
}
/// Returns the accessible-value of that element, if any.
std::optional<SharedString> accessible_value() const
{
SharedString result;
if (inner.item_tree.vtable()->accessible_string_property(
inner.item_tree.borrow(), inner.index,
cbindgen_private::AccessibleStringProperty::Value, &result)) {
return result;
} else {
return std::nullopt;
}
}
/// Sets the accessible-value of that element.
///
/// Setting the value will invoke the `accessible-action-set-value` callback.
void set_accessible_value(SharedString value) const
{
union SetValueHelper {
cbindgen_private::AccessibilityAction action;
SetValueHelper(SharedString value)
// : action { .set_value = { cbindgen_private::AccessibilityAction::Tag::SetValue,
// std::move(value) } }
{
new (&action.set_value) cbindgen_private::AccessibilityAction::SetValue_Body {
cbindgen_private::AccessibilityAction::Tag::SetValue, std::move(value)
};
}
~SetValueHelper() { action.set_value.~SetValue_Body(); }
} action(std::move(value));
inner.item_tree.vtable()->accessibility_action(inner.item_tree.borrow(), inner.index,
&action.action);
}
/// Invokes the default accessibility action of that element (`accessible-action-default`).
void invoke_default_action() const
{
union DefaultActionHelper {
cbindgen_private::AccessibilityAction action;
DefaultActionHelper()
//: action { .tag = cbindgen_private::AccessibilityAction::Tag::Default }
{
action.tag = cbindgen_private::AccessibilityAction::Tag::Default;
}
~DefaultActionHelper() { }
} action;
inner.item_tree.vtable()->accessibility_action(inner.item_tree.borrow(), inner.index,
&action.action);
}
/// Returns the size of this element
LogicalSize size() const
{
auto rect = inner.item_tree.vtable()->item_geometry(inner.item_tree.borrow(), inner.index);
return LogicalSize({ rect.width, rect.height });
}
/// Returns the absolute position of this element
LogicalPosition absolute_position() const
{
cbindgen_private::LogicalRect rect =
inner.item_tree.vtable()->item_geometry(inner.item_tree.borrow(), inner.index);
cbindgen_private::LogicalPoint abs = slint::cbindgen_private::slint_item_absolute_position(
&inner.item_tree, inner.index);
return LogicalPosition({ abs.x + rect.x, abs.y + rect.y });
}
};
}
#endif // SLINT_FEATURE_TESTING

View file

@ -2,19 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial
#pragma once #pragma once
#include "slint.h" #include "slint-testing.h"
#include <concepts>
#include <iostream> #include <iostream>
// this file contains function useful for internal testing // this file contains function useful for internal testing
namespace slint::private_api::testing { namespace slint::private_api::testing {
inline void init()
{
cbindgen_private::slint_testing_init_backend();
}
inline void mock_elapsed_time(int64_t time_in_ms) inline void mock_elapsed_time(int64_t time_in_ms)
{ {
cbindgen_private::slint_mock_elapsed_time(time_in_ms); cbindgen_private::slint_mock_elapsed_time(time_in_ms);

View file

@ -24,8 +24,10 @@ pub fn with_platform<R>(
i_slint_core::with_platform(|| Err(i_slint_core::platform::PlatformError::NoPlatform), f) i_slint_core::with_platform(|| Err(i_slint_core::platform::PlatformError::NoPlatform), f)
} }
/// One need to make sure something from the crate is exported, // One need to make sure something from the crate is exported,
/// otherwise its symbols are not going to be in the final binary // otherwise its symbols are not going to be in the final binary
#[cfg(feature = "testing")]
pub use i_slint_backend_testing;
#[cfg(feature = "slint-interpreter")] #[cfg(feature = "slint-interpreter")]
pub use slint_interpreter; pub use slint_interpreter;
@ -138,12 +140,6 @@ pub unsafe extern "C" fn slint_register_bitmap_font(
window_adapter.renderer().register_bitmap_font(font_data); window_adapter.renderer().register_bitmap_font(font_data);
} }
#[cfg(feature = "internal-testing")]
#[no_mangle]
pub unsafe extern "C" fn slint_testing_init_backend() {
i_slint_backend_testing::init_no_event_loop();
}
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
mod allocator { mod allocator {
use core::alloc::Layout; use core::alloc::Layout;

View file

@ -19,6 +19,8 @@ path = "lib.rs"
[features] [features]
# Internal feature that is only enabled for Slint's own tests # Internal feature that is only enabled for Slint's own tests
internal = [] internal = []
# ffi for C++ bindings
ffi = []
[dependencies] [dependencies]
i-slint-core = { workspace = true } i-slint-core = { workspace = true }

View file

@ -0,0 +1,25 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial
use i_slint_core::accessibility::AccessibleStringProperty;
use i_slint_core::item_tree::ItemTreeRc;
use i_slint_core::items::ItemRc;
use i_slint_core::slice::Slice;
use i_slint_core::SharedVector;
#[no_mangle]
pub extern "C" fn slint_testing_init_backend() {
crate::init_integration_test();
}
#[no_mangle]
pub extern "C" fn slint_testing_element_find_by_accessible_label(
root: &ItemTreeRc,
label: &Slice<u8>,
out: &mut SharedVector<ItemRc>,
) {
let Ok(label) = core::str::from_utf8(label.as_slice()) else { return };
*out = crate::search_api::search_item(root, |item| {
item.accessible_string_property(AccessibleStringProperty::Label).is_some_and(|x| x == label)
})
}

View file

@ -13,6 +13,8 @@ pub use internal_tests::*;
mod testing_backend; mod testing_backend;
#[cfg(feature = "internal")] #[cfg(feature = "internal")]
pub use testing_backend::*; pub use testing_backend::*;
#[cfg(feature = "ffi")]
mod ffi;
/// Initialize the testing backend without support for event loop. /// Initialize the testing backend without support for event loop.
/// This means that each test thread can use its own backend, but global functions that needs /// This means that each test thread can use its own backend, but global functions that needs

View file

@ -5,10 +5,13 @@ use i_slint_core::accessibility::{AccessibilityAction, AccessibleStringProperty}
use i_slint_core::item_tree::{ItemTreeRc, ItemVisitorResult, TraversalOrder}; use i_slint_core::item_tree::{ItemTreeRc, ItemVisitorResult, TraversalOrder};
use i_slint_core::items::ItemRc; use i_slint_core::items::ItemRc;
use i_slint_core::window::WindowInner; use i_slint_core::window::WindowInner;
use i_slint_core::SharedString; use i_slint_core::{SharedString, SharedVector};
fn search_item(item_tree: &ItemTreeRc, mut filter: impl FnMut(&ItemRc) -> bool) -> Vec<ItemRc> { pub(crate) fn search_item(
let mut result = vec![]; item_tree: &ItemTreeRc,
mut filter: impl FnMut(&ItemRc) -> bool,
) -> SharedVector<ItemRc> {
let mut result = SharedVector::default();
i_slint_core::item_tree::visit_items( i_slint_core::item_tree::visit_items(
item_tree, item_tree,
TraversalOrder::BackToFront, TraversalOrder::BackToFront,

View file

@ -46,7 +46,27 @@ assert_eq!(button.absolute_position(), slint::LogicalPosition::new(123., 143.));
assert_eq!(button.size(), slint::LogicalSize::new(143., 76.)); assert_eq!(button.size(), slint::LogicalSize::new(143., 76.));
assert_eq!(button.accessible_value().unwrap(), "0"); assert_eq!(button.accessible_value().unwrap(), "0");
button.set_accessible_value("45".into()); instance.invoke_call();
assert_eq!(button.accessible_value().unwrap(), "45"); assert_eq!(button.accessible_value().unwrap(), "45");
button.set_accessible_value("78".into());
assert_eq!(button.accessible_value().unwrap(), "78");
```
```cpp
auto handle = TestCase::create();
const TestCase &instance = *handle;
auto button_search = slint::testing::ElementHandle::find_by_accessible_label(handle, "Hello");
assert_eq(button_search.size(), 1);
auto button = button_search[0];
assert(button.absolute_position() == slint::LogicalPosition({123., 143.}));
assert(button.size() == slint::LogicalSize({143., 76.}));
assert_eq(button.accessible_value().value(), "0");
instance.invoke_call();
assert_eq(button.accessible_value().value(), "45");
button.set_accessible_value("78");
assert_eq(button.accessible_value().value(), "78");
``` ```
*/ */

View file

@ -18,7 +18,7 @@ path = "main.rs"
name = "test-driver-cpp" name = "test-driver-cpp"
[dependencies] [dependencies]
slint-cpp = { workspace = true, features = ["internal-testing", "std"] } slint-cpp = { workspace = true, features = ["testing", "std"] }
[dev-dependencies] [dev-dependencies]
i-slint-compiler = { workspace = true, features = ["default", "cpp", "display-diagnostics"] } i-slint-compiler = { workspace = true, features = ["default", "cpp", "display-diagnostics"] }

View file

@ -55,7 +55,7 @@ pub fn test(testcase: &test_driver_lib::TestCase) -> Result<(), Box<dyn Error>>
namespace slint_testing = slint::private_api::testing; namespace slint_testing = slint::private_api::testing;
", ",
)?; )?;
generated_cpp.write_all(b"int main() {\n slint::private_api::testing::init();\n")?; generated_cpp.write_all(b"int main() {\n slint::testing::init();\n")?;
for x in test_driver_lib::extract_test_functions(&source).filter(|x| x.language_id == "cpp") { for x in test_driver_lib::extract_test_functions(&source).filter(|x| x.language_id == "cpp") {
write!(generated_cpp, " {{\n {}\n }}\n", x.source.replace("\n", "\n "))?; write!(generated_cpp, " {{\n {}\n }}\n", x.source.replace("\n", "\n "))?;
} }

View file

@ -91,6 +91,7 @@ pub fn generate(show_warnings: bool) -> Result<(), Box<dyn std::error::Error>> {
renderer_skia: true, renderer_skia: true,
experimental: false, experimental: false,
gettext: true, gettext: true,
testing: true,
}; };
cbindgen::gen_all(&root, &generated_headers_dir, enabled_features)?; cbindgen::gen_all(&root, &generated_headers_dir, enabled_features)?;