#![cfg(feature = "macos_14_0")]
use screencapturekit::content_sharing_picker::SCContentSharingPickerConfiguration;
#[test]
fn test_picker_configuration_new() {
let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
assert!(!config.as_ptr().is_null());
println!("✓ Picker configuration created");
}
#[test]
fn test_picker_configuration_default_from_system() {
let config = SCContentSharingPickerConfiguration::default_from_system()
.expect("read the default picker configuration");
assert!(
!config.as_ptr().is_null(),
"default_from_system() returned a null configuration pointer"
);
let cloned = config.clone();
assert!(!cloned.as_ptr().is_null());
drop(cloned);
let mut config = config;
config
.set_excluded_bundle_ids(&["com.apple.dock"])
.expect("bundle IDs have no NUL byte");
drop(config);
}
#[test]
fn test_picker_configuration_clone() {
let config1 = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
let config2 = config1.clone();
assert!(!config1.as_ptr().is_null());
assert!(!config2.as_ptr().is_null());
println!("✓ Picker configuration clone works");
}
#[test]
fn test_picker_configuration_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<SCContentSharingPickerConfiguration>();
assert_sync::<SCContentSharingPickerConfiguration>();
println!("✓ SCContentSharingPickerConfiguration is Send + Sync");
}
#[test]
fn test_picker_configuration_lifecycle() {
for i in 0..3 {
let config =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
assert!(!config.as_ptr().is_null());
println!("✓ Configuration {i} created");
drop(config);
println!("✓ Configuration {i} dropped");
}
}
#[test]
fn test_picker_configuration_modes() {
use screencapturekit::content_sharing_picker::SCContentSharingPickerMode;
let mut config =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
let modes = [
SCContentSharingPickerMode::SingleWindow,
SCContentSharingPickerMode::SingleDisplay,
];
config.set_allowed_picker_modes(&modes);
assert_eq!(config.allowed_picker_modes(), modes);
assert!(!config.as_ptr().is_null());
println!("✓ Picker modes round-trip successfully");
}
#[test]
fn test_picker_api_availability() {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
let _picker_type = std::any::type_name::<SCContentSharingPicker>();
let _config_type = std::any::type_name::<SCContentSharingPickerConfiguration>();
println!("✓ Content sharing picker API available on macOS 14.0+");
}
#[test]
#[cfg(feature = "async")]
fn test_async_picker_types_exist() {
use screencapturekit::async_api::{
AsyncPickerFilterFuture, AsyncPickerFuture, AsyncSCContentSharingPicker,
};
let _picker_type = std::any::type_name::<AsyncSCContentSharingPicker>();
let _future_type = std::any::type_name::<AsyncPickerFuture>();
let _filter_future_type = std::any::type_name::<AsyncPickerFilterFuture>();
println!("✓ Async picker types available");
}
#[test]
#[cfg(feature = "async")]
fn test_async_picker_future_is_future() {
use screencapturekit::async_api::AsyncPickerFuture;
use std::future::Future;
fn assert_future<T: Future>() {}
assert_future::<AsyncPickerFuture>();
println!("✓ AsyncPickerFuture implements Future");
}
#[test]
#[cfg(feature = "async")]
fn test_async_picker_filter_future_is_future() {
use screencapturekit::async_api::AsyncPickerFilterFuture;
use std::future::Future;
fn assert_future<T: Future>() {}
assert_future::<AsyncPickerFilterFuture>();
println!("✓ AsyncPickerFilterFuture implements Future");
}
#[test]
fn test_picker_is_active_get_set_roundtrip() {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
let _guard = exclusive_registry();
let original = SCContentSharingPicker::is_active();
SCContentSharingPicker::set_active(true).expect("picker is available");
assert!(
SCContentSharingPicker::is_active(),
"is_active() returned false immediately after set_active(true)"
);
SCContentSharingPicker::set_active(false).expect("picker is available");
assert!(
!SCContentSharingPicker::is_active(),
"is_active() returned true immediately after set_active(false)"
);
SCContentSharingPicker::set_active(original).expect("picker is available");
}
static PICKER_STATE: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn exclusive_registry() -> std::sync::MutexGuard<'static, ()> {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
let guard = PICKER_STATE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
SCContentSharingPicker::remove_all_observers();
guard
}
fn observe(
handler: impl Fn(screencapturekit::content_sharing_picker::SCPickerEvent) + Send + Sync + 'static,
) -> Option<screencapturekit::content_sharing_picker::SCPickerSubscription> {
use screencapturekit::content_sharing_picker::{
SCContentSharingPicker, SCPickerConfigurationError,
};
match SCContentSharingPicker::add_observer(handler) {
Ok(subscription) => Some(subscription),
Err(SCPickerConfigurationError::MainThreadRequired) => None,
Err(error) => panic!("unexpected picker error: {error}"),
}
}
#[test]
fn add_observer_returns_a_live_subscription() {
let _guard = exclusive_registry();
let Some(subscription) = observe(|_event| {}) else {
return;
};
assert!(
subscription.is_active(),
"add_observer returned an inactive subscription"
);
assert_ne!(
subscription.token(),
0,
"live subscription must have a token"
);
assert!(
subscription.unsubscribe(),
"unsubscribe reported no removal"
);
}
#[test]
fn dropping_a_subscription_removes_the_observer() {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
let _guard = exclusive_registry();
let Some(subscription) = observe(|_event| {}) else {
assert_eq!(SCContentSharingPicker::remove_all_observers(), 0);
return;
};
assert_ne!(subscription.token(), 0);
drop(subscription);
assert_eq!(
SCContentSharingPicker::remove_all_observers(),
0,
"observer survived the drop of its subscription"
);
}
#[test]
fn unsubscribing_twice_is_not_reported_twice() {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
let _guard = exclusive_registry();
let Some(subscription) = observe(|_event| {}) else {
return;
};
let was_active = subscription.is_active();
assert_eq!(subscription.unsubscribe(), was_active);
assert_eq!(SCContentSharingPicker::remove_all_observers(), 0);
}
#[test]
fn remove_all_observers_sweeps_detached_registrations() {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
let _guard = exclusive_registry();
let (Some(first), Some(second)) = (observe(|_event| {}), observe(|_event| {})) else {
return;
};
let expected = usize::from(first.is_active()) + usize::from(second.is_active());
first.detach();
second.detach();
assert_eq!(
SCContentSharingPicker::remove_all_observers(),
expected,
"detached observers were not swept"
);
}
#[test]
fn remove_all_observers_invalidates_live_subscriptions() {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
let _guard = exclusive_registry();
let (Some(first), Some(second)) = (observe(|_event| {}), observe(|_event| {})) else {
return;
};
let expected = usize::from(first.is_active()) + usize::from(second.is_active());
assert_eq!(SCContentSharingPicker::remove_all_observers(), expected);
assert!(!first.is_active());
assert!(!second.is_active());
}
#[test]
fn add_observer_without_a_main_run_loop_is_an_error() {
use screencapturekit::content_sharing_picker::{
SCContentSharingPicker, SCPickerConfigurationError,
};
use std::sync::Arc;
let _guard = exclusive_registry();
let captured = Arc::new(());
let handler_state = Arc::clone(&captured);
let result = std::thread::spawn(move || {
SCContentSharingPicker::add_observer(move |_event| {
let _ = &handler_state;
})
.map(|subscription| subscription.token())
})
.join()
.expect("registration thread panicked");
assert_eq!(result, Err(SCPickerConfigurationError::MainThreadRequired));
assert_eq!(Arc::strong_count(&captured), 1, "rejected handler was kept");
}
#[test]
fn observer_registration_does_not_leak_across_many_cycles() {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let _guard = exclusive_registry();
let counter = Arc::new(AtomicUsize::new(0));
for _ in 0..16 {
let counter = Arc::clone(&counter);
let subscription = observe(move |_event| {
counter.fetch_add(1, Ordering::Relaxed);
});
drop(subscription);
}
assert_eq!(SCContentSharingPicker::remove_all_observers(), 0);
assert_eq!(
Arc::strong_count(&counter),
1,
"observer contexts retained captured state after teardown"
);
assert_eq!(
counter.load(Ordering::Relaxed),
0,
"an observer fired without any user interaction"
);
}
#[test]
fn set_default_configuration_is_reflected_by_default_configuration() {
use screencapturekit::content_sharing_picker::{
SCContentSharingPicker, SCContentSharingPickerConfiguration, SCPickerConfigurationError,
};
if !SCContentSharingPicker::is_available() {
return;
}
let mut config =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
config.set_allows_changing_selected_content(true);
config
.set_excluded_bundle_ids(&["com.example.picker-test"])
.expect("bundle IDs have no NUL byte");
match SCContentSharingPicker::set_default_configuration(&config) {
Ok(()) => {}
Err(SCPickerConfigurationError::MainThreadRequired) => return,
Err(error) => panic!("unexpected configuration error: {error}"),
}
let read_back = SCContentSharingPicker::default_configuration()
.expect("read the default picker configuration");
assert!(read_back.allows_changing_selected_content());
assert_eq!(read_back.excluded_bundle_ids(), ["com.example.picker-test"]);
}
#[test]
fn configuration_setter_rejects_worker_thread() {
use screencapturekit::content_sharing_picker::{
SCContentSharingPicker, SCContentSharingPickerConfiguration, SCPickerConfigurationError,
};
if !SCContentSharingPicker::is_available() {
return;
}
let config = SCContentSharingPickerConfiguration::new().expect("create picker configuration");
let result =
std::thread::spawn(move || SCContentSharingPicker::set_default_configuration(&config))
.join()
.expect("worker thread panicked");
assert_eq!(result, Err(SCPickerConfigurationError::MainThreadRequired));
}
#[test]
fn configuration_setters_never_dispatch_source_contract() {
let source =
include_str!("../swift-bridge/Sources/ScreenCaptureKitBridge/ContentSharingPicker.swift");
let section = source
.split("// MARK: - Standalone configuration operations")
.nth(1)
.expect("configuration section")
.split("// MARK: - Standalone present operations")
.next()
.expect("configuration section end");
assert_eq!(
section
.matches("guard Thread.isMainThread else { return false }")
.count(),
2
);
assert!(!section.contains("CFRunLoopCopyCurrentMode"));
assert!(!section.contains("DispatchQueue.main"));
assert!(!section.contains("performOnMainSynchronously"));
}
#[test]
fn excluded_bundle_ids_round_trip() {
use screencapturekit::content_sharing_picker::SCContentSharingPickerConfiguration;
let mut config =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
let ids = ["com.apple.dock", "com.apple.finder"];
config
.set_excluded_bundle_ids(&ids)
.expect("bundle IDs have no NUL byte");
assert_eq!(config.excluded_bundle_ids_count(), ids.len());
assert_eq!(config.excluded_bundle_ids(), ids);
}
#[test]
fn excluded_bundle_ids_with_interior_nul_are_rejected() {
use screencapturekit::content_sharing_picker::SCContentSharingPickerConfiguration;
let mut config =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
config
.set_excluded_bundle_ids(&["com.apple.dock"])
.expect("bundle IDs have no NUL byte");
assert!(config
.set_excluded_bundle_ids(&["com.apple.finder", "com.bad\0id"])
.is_err());
assert_eq!(config.excluded_bundle_ids(), ["com.apple.dock"]);
}
#[test]
fn excluded_window_ids_round_trip() {
use screencapturekit::content_sharing_picker::SCContentSharingPickerConfiguration;
let mut config =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
config.set_excluded_window_ids(&[7, 42, 1009]);
assert_eq!(config.excluded_window_ids(), vec![7, 42, 1009]);
}
#[test]
fn deactivate_clears_the_active_flag() {
use screencapturekit::content_sharing_picker::SCContentSharingPicker;
let _guard = exclusive_registry();
SCContentSharingPicker::set_active(true).expect("picker is available");
SCContentSharingPicker::deactivate();
for _ in 0..20 {
if !SCContentSharingPicker::is_active() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
SCContentSharingPicker::set_active(false).expect("picker is available");
assert!(!SCContentSharingPicker::is_active());
}
#[test]
fn clone_does_not_alias_the_original() {
use screencapturekit::content_sharing_picker::SCContentSharingPickerConfiguration;
let mut original =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
original
.set_excluded_bundle_ids(&["com.example.original"])
.expect("bundle IDs have no NUL byte");
original.set_excluded_window_ids(&[1]);
original.set_allows_changing_selected_content(false);
let mut copy = original.clone();
copy.set_excluded_bundle_ids(&["com.example.copy"])
.expect("bundle IDs have no NUL byte");
copy.set_excluded_window_ids(&[2, 3]);
copy.set_allows_changing_selected_content(true);
assert_eq!(original.excluded_bundle_ids(), ["com.example.original"]);
assert_eq!(original.excluded_window_ids(), vec![1]);
assert!(!original.allows_changing_selected_content());
assert_eq!(copy.excluded_bundle_ids(), ["com.example.copy"]);
assert_eq!(copy.excluded_window_ids(), vec![2, 3]);
assert!(copy.allows_changing_selected_content());
}
#[test]
fn clone_carries_the_source_values_forward() {
use screencapturekit::content_sharing_picker::{
SCContentSharingPickerConfiguration, SCContentSharingPickerMode,
};
let mut original =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
let modes = [
SCContentSharingPickerMode::SingleDisplay,
SCContentSharingPickerMode::MultipleWindows,
];
original.set_allowed_picker_modes(&modes);
original
.set_excluded_bundle_ids(&["com.apple.dock"])
.expect("bundle IDs have no NUL byte");
original.set_excluded_window_ids(&[11, 22]);
original.set_allows_changing_selected_content(true);
let copy = original.clone();
assert_eq!(copy.allowed_picker_modes(), original.allowed_picker_modes());
assert_eq!(copy.excluded_bundle_ids(), original.excluded_bundle_ids());
assert_eq!(copy.excluded_window_ids(), original.excluded_window_ids());
assert_eq!(
copy.allows_changing_selected_content(),
original.allows_changing_selected_content()
);
}
#[test]
fn clone_survives_the_original_being_dropped() {
use screencapturekit::content_sharing_picker::SCContentSharingPickerConfiguration;
let copy = {
let mut original =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
original
.set_excluded_bundle_ids(&["com.example.scoped"])
.expect("bundle IDs have no NUL byte");
original.clone()
};
assert_eq!(copy.excluded_bundle_ids(), ["com.example.scoped"]);
}
#[test]
fn clones_are_independently_mutable_across_threads() {
use screencapturekit::content_sharing_picker::SCContentSharingPickerConfiguration;
let mut template =
SCContentSharingPickerConfiguration::new().expect("create picker configuration");
template.set_excluded_window_ids(&[0]);
let handles: Vec<_> = (1..=4u32)
.map(|i| {
let mut config = template.clone();
std::thread::spawn(move || {
for _ in 0..64 {
config.set_excluded_window_ids(&[i, i * 10]);
assert_eq!(config.excluded_window_ids(), vec![i, i * 10]);
}
})
})
.collect();
for handle in handles {
handle.join().expect("worker thread panicked");
}
assert_eq!(template.excluded_window_ids(), vec![0]);
}