use serde::Serialize;
use wabot_feature_rest_controller::axum::http::StatusCode;
use wabot_feature_rest_controller::axum::Router;
use wabot_feature_ui_controller::nav::NAV_HEADER;
use wabot_feature_ui_controller::runtime::action_route;
use crate::rest::{RestHarness, TestResponse};
#[derive(Clone)]
pub struct UiHarness {
rest: RestHarness,
}
impl UiHarness {
pub fn new(router: Router) -> Self {
Self {
rest: RestHarness::new(router),
}
}
pub fn rest(&self) -> &RestHarness {
&self.rest
}
pub fn with_header(&self, name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
rest: self.rest.with_header(name, value),
}
}
pub fn with_bearer(&self, token: impl std::fmt::Display) -> Self {
Self {
rest: self.rest.with_bearer(token),
}
}
pub fn with_cookie(&self, name: &str, value: impl std::fmt::Display) -> Self {
Self {
rest: self.rest.with_cookie(name, value),
}
}
pub async fn get(&self, path: &str) -> Page {
Page(self.rest.get(path).send().await)
}
pub async fn navigate(&self, path: &str) -> Fragment {
Fragment(self.rest.get(path).header(NAV_HEADER, "1").send().await)
}
pub async fn action<T: Serialize>(&self, base: &str, name: &str, body: &T) -> TestResponse {
self.rest
.post(&action_route(base, name))
.json(body)
.send()
.await
}
pub async fn client_runtime(&self) -> TestResponse {
self.rest
.get(wabot_feature_ui_controller::nav::CLIENT_RUNTIME_PATH)
.send()
.await
}
}
pub struct Page(pub TestResponse);
impl Page {
pub fn status(&self) -> StatusCode {
self.0.status
}
pub fn html(&self) -> &str {
&self.0.body
}
pub fn contains(&self, fragment: &str) -> bool {
self.0.body.contains(fragment)
}
pub fn has_island(&self, id: &str) -> bool {
self.0
.body
.contains(&format!("data-island=\"{}\"", html_escape(id)))
}
pub fn island_props(&self, id: &str) -> Option<serde_json::Value> {
let marker = format!("data-island=\"{}\"", html_escape(id));
let start = self.0.body.find(&marker)? + marker.len();
let rest = &self.0.body[start..];
let props_at = rest.find("data-props=\"")? + "data-props=\"".len();
let rest = &rest[props_at..];
let end = rest.find('"')?;
serde_json::from_str(&html_unescape(&rest[..end])).ok()
}
pub fn islands(&self) -> Vec<String> {
let mut ids = Vec::new();
let mut rest = self.0.body.as_str();
while let Some(at) = rest.find("data-island=\"") {
rest = &rest[at + "data-island=\"".len()..];
if let Some(end) = rest.find('"') {
ids.push(html_unescape(&rest[..end]));
rest = &rest[end..];
} else {
break;
}
}
ids
}
pub fn header(&self, name: &str) -> Option<&str> {
self.0.header(name)
}
pub fn assert_status(&self, expected: StatusCode) -> &Self {
self.0.assert_status(expected);
self
}
pub fn assert_ok(&self) -> &Self {
self.assert_status(StatusCode::OK)
}
pub fn assert_contains(&self, fragment: &str) -> &Self {
assert!(
self.contains(fragment),
"expected the page to contain {fragment:?}, got:\n{}",
self.0.body
);
self
}
}
pub struct Fragment(pub TestResponse);
impl Fragment {
pub fn status(&self) -> StatusCode {
self.0.status
}
pub fn payload(&self) -> serde_json::Value {
self.0.value()
}
pub fn html(&self) -> String {
self.payload()["html"]
.as_str()
.unwrap_or_default()
.to_string()
}
pub fn title(&self) -> Option<String> {
self.payload()["title"].as_str().map(str::to_string)
}
pub fn scripts(&self) -> Vec<String> {
self.payload()["scripts"]
.as_array()
.map(|items| {
items
.iter()
.filter_map(|s| s.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
pub fn assert_ok(&self) -> &Self {
self.0.assert_ok();
self
}
}
fn html_escape(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn html_unescape(value: &str) -> String {
value
.replace(""", "\"")
.replace(">", ">")
.replace("<", "<")
.replace("&", "&")
}