use std::time::Duration;
use kernal_api::async_engine;
use kernal_api::webview::{
BestEffort, ExternalWebviewClient, WebviewError, WebviewHandle, WebviewTestPresentation,
WebviewUrlGrant, WebviewWindowOptions, WindowOptionsError,
};
pub const WIDGET_APP_ID: &str = "dev.kernal-api.smoke-widget";
pub const NAVIGATED_QUERY: &str = "?navigated=1";
const INITIAL_POSITION: (i32, i32) = (40, 60);
const MOVED_POSITION: (i32, i32) = (120, 90);
pub fn widget_options() -> Result<WebviewWindowOptions, WebviewError> {
widget_options_sized(360, 240)
}
fn widget_options_sized(width: u32, height: u32) -> Result<WebviewWindowOptions, WebviewError> {
WebviewWindowOptions::new("kernal-api widget", width, height)
.and_then(|options| options.initial_position(INITIAL_POSITION.0, INITIAL_POSITION.1))
.map_err(|error| WebviewError::HostFailure(error.to_string()))
.map(|options| {
options
.decorations(false)
.transparent(true)
.always_on_top(true)
.skip_taskbar(true)
})
}
fn keep_above_is_display_policy() -> bool {
std::env::var_os("DISPLAY").is_some() || std::env::var_os("WAYLAND_DISPLAY").is_some()
}
async fn await_presentation(
webview: &WebviewHandle,
what: &str,
ready: impl Fn(&WebviewTestPresentation) -> bool,
) -> Result<WebviewTestPresentation, WebviewError> {
let deadline = std::time::Instant::now() + Duration::from_secs(5);
loop {
let presentation = webview.presentation_for_test()?;
if ready(&presentation) {
return Ok(presentation);
}
if std::time::Instant::now() >= deadline {
return Err(WebviewError::HostFailure(format!(
"window did not become {what}: {presentation:?}"
)));
}
async_engine::sleep(Duration::from_millis(20)).await;
}
}
fn at(presentation: &WebviewTestPresentation, (x, y): (i32, i32)) -> bool {
(presentation.logical_x - f64::from(x)).abs() <= 1.0
&& (presentation.logical_y - f64::from(y)).abs() <= 1.0
}
fn mismatch(message: impl Into<String>) -> WebviewError {
WebviewError::HostFailure(message.into())
}
pub async fn widget_controls(
client: &ExternalWebviewClient,
webview: WebviewHandle,
url: &str,
) -> Result<(), WebviewError> {
let options = widget_options()?;
let support = client.window_support();
eprintln!("widget window support: {support:?}");
webview.verify_window_options_for_test(&options)?;
let initial = webview.presentation_for_test()?;
eprintln!("widget initial presentation: {initial:?}");
if initial.decorated {
return Err(mismatch("undecorated widget reports decorations"));
}
if !keep_above_is_display_policy() && !initial.always_on_top {
return Err(mismatch(
"keep-above request did not reach the native window",
));
}
if support.position == BestEffort::Requested {
await_presentation(&webview, "at its initial position", |presentation| {
at(presentation, INITIAL_POSITION)
})
.await?;
}
if webview.set_size(0, 240).await
!= Err(WebviewError::InvalidWindowOptions(
WindowOptionsError::InvalidSize,
))
{
return Err(mismatch(
"invalid resize was not rejected before native effects",
));
}
webview.set_size(480, 320).await?;
let resized = await_presentation(&webview, "480x320", |presentation| {
(presentation.logical_width - 480.0).abs() <= 1.0
&& (presentation.logical_height - 320.0).abs() <= 1.0
})
.await?;
eprintln!("widget resized: {resized:?}");
if webview.set_position(32768, 0).await
!= Err(WebviewError::InvalidWindowOptions(
WindowOptionsError::InvalidPosition,
))
{
return Err(mismatch(
"invalid move was not rejected before native effects",
));
}
let moved = webview
.set_position(MOVED_POSITION.0, MOVED_POSITION.1)
.await?;
if moved != support.position {
return Err(mismatch(format!(
"set_position reported {moved:?}, host support is {:?}",
support.position
)));
}
if moved == BestEffort::Requested {
let moved = await_presentation(&webview, "moved", |presentation| {
at(presentation, MOVED_POSITION)
})
.await?;
eprintln!("widget moved: {moved:?}");
}
webview.hide().await?;
await_presentation(&webview, "hidden", |presentation| !presentation.visible).await?;
webview.show().await?;
await_presentation(&webview, "visible", |presentation| presentation.visible).await?;
webview.verify_window_options_for_test(&widget_options_sized(480, 320)?)?;
webview.focus().await?;
let navigated = WebviewUrlGrant::new(&format!("{url}{NAVIGATED_QUERY}"))?;
webview.navigate(&navigated).await?;
webview.wait_until_loaded(Duration::from_secs(30)).await?;
let observation = client.test_observation();
if observation.live_resources != 1 || observation.native_backings != 1 {
return Err(mismatch(format!(
"window operations changed view ownership: {observation:?}"
)));
}
webview.close().await?;
super::assert_clean(client)
}