use std::time::Instant;
use super::utils;
use rama::{
futures::StreamExt,
http::{
BodyExtractExt, StatusCode,
headers::{ContentType, HeaderMapExt},
mime,
sse::{
JsonEventData,
datastar::{DatastarEvent, EventType, PatchElements},
},
},
utils::str::non_empty_str,
};
use itertools::Itertools;
use serde::Deserialize;
use serde_json::json;
#[tokio::test]
#[ignore]
async fn test_http_sse_datastar_hello() {
utils::init_tracing();
let runner = utils::ExampleRunner::interactive("http_sse_datastar_hello", None);
let index_response = runner.get("http://127.0.0.1:62051").send().await.unwrap();
assert_eq!(StatusCode::OK, index_response.status());
assert!(
index_response
.headers()
.typed_get::<ContentType>()
.map(|ct| ct.mime().eq(&mime::TEXT_HTML_UTF_8))
.unwrap_or_default()
);
let index_content = index_response.try_into_string().await.unwrap();
assert!(index_content.contains(r##"<h1>🦙💬 "hello 🚀 datastar"</h1>"##));
let script_rsponse = runner
.get("http://127.0.0.1:62051/assets/datastar.js")
.send()
.await
.unwrap();
assert_eq!(StatusCode::OK, script_rsponse.status());
assert!(
script_rsponse
.headers()
.typed_get::<ContentType>()
.map(|ct| ct.mime().eq(&mime::APPLICATION_JAVASCRIPT_UTF_8))
.unwrap_or_default()
);
let script_content = script_rsponse.try_into_string().await.unwrap();
assert!(script_content.contains(r##"// Datastar v1"##));
let start_ts = Instant::now();
let mut hotreload_stream = runner
.get("http://127.0.0.1:62051/hotreload")
.send()
.await
.unwrap()
.into_body()
.into_event_stream();
let hotreload_event: DatastarEvent = hotreload_stream
.next()
.await
.expect("one event")
.expect("valid datastar event");
let hotreload_execute_script = hotreload_event
.into_data()
.unwrap()
.into_patch_elements()
.unwrap();
assert!(
hotreload_execute_script
.elements
.unwrap()
.contains("window.location.reload()")
);
let mut expected_events: Vec<TestEvent> = vec![
PatchElements::new_remove(non_empty_str!("#server-warning"))
.try_into()
.unwrap(),
PatchElements::new(non_empty_str!(
r##"
<div id='message'>Hello, Datastar!</div>
<div id="progress-bar" style="width: 100%"></div>
"##,
))
.try_into()
.unwrap(),
];
const MESSAGE: &str = "Hello, Datastar!";
for i in 1..=MESSAGE.len() {
let text = &MESSAGE[..i];
let progress = (i as f64) / (MESSAGE.len() as f64) * 100f64;
expected_events.push(
PatchElements::new(
format!(
r##"
<div id='message'>{text}</div>
<div id="progress-bar" style="width: {progress}%"></div>
"##
)
.try_into()
.unwrap(),
)
.try_into()
.unwrap(),
)
}
expected_events.reverse();
let mut sse_status_counter = 0;
let mut signal_counter = 0;
let mut stream = runner
.get("http://127.0.0.1:62051/hello-world")
.send()
.await
.unwrap()
.into_body()
.into_event_stream();
let response = runner
.post("http://127.0.0.1:62051/start?datastar=")
.json(&json!({
"delay": 1,
}))
.send()
.await
.unwrap();
assert_eq!(StatusCode::OK, response.status());
let mut index = 0;
while let Some(result) = stream.next().await {
let event: TestEvent = result.unwrap();
index += 1;
println!("#{index}> >>RECVD: {event:?}");
if event.event() == Some(EventType::PatchSignals.as_str()) {
let data = event.into_data().unwrap().into_patch_signals().unwrap();
assert!(!data.only_if_missing);
if signal_counter == 0 {
assert!([400, 1].contains(&data.signals.delay.unwrap()));
} else {
assert_eq!(1, data.signals.delay.unwrap());
}
signal_counter += 1;
continue;
}
if event
.data()
.cloned()
.and_then(|d| d.into_patch_elements().ok())
.map(|d| {
d.elements
.as_deref()
.unwrap_or_default()
.contains("sse-status")
})
.unwrap_or_default()
{
sse_status_counter += 1;
continue;
}
match expected_events.pop() {
Some(expected_event) => {
if expected_event.event() == Some(EventType::PatchElements.as_str()) {
assert_eq!(
expected_event.event(),
event.event(),
"event #{index}: {event:?}"
);
assert_eq!(expected_event.id(), event.id(), "event #{index}: {event:?}");
assert_eq!(
expected_event.retry(),
event.retry(),
"event #{index}: {event:?}"
);
assert_eq!(
expected_event.comment().collect_vec(),
event.comment().collect_vec(),
"event #{index}: {event:?}"
);
let expected_data = expected_event
.into_data()
.unwrap()
.into_patch_elements()
.unwrap();
let data = event.into_data().unwrap().into_patch_elements().unwrap();
assert_eq!(expected_data.selector, data.selector);
assert_eq!(expected_data.mode, data.mode);
assert_eq!(expected_data.use_view_transition, data.use_view_transition);
if expected_data
.elements
.as_deref()
.unwrap_or_default()
.contains("sse-status")
{
assert!(
data.elements
.as_deref()
.unwrap_or_default()
.contains("sse-status"),
"event #{index}: data = {data:?}"
);
} else {
let mut expected_elements = expected_data
.elements
.as_deref()
.unwrap_or_default()
.to_owned();
expected_elements.retain(|c| !c.is_whitespace());
let mut elements = data.elements.as_deref().unwrap_or_default().to_owned();
elements.retain(|c| !c.is_whitespace());
assert_eq!(expected_elements, elements, "event #{index}");
}
} else {
assert_eq!(expected_event, event);
}
}
None => panic!("unexpected stream event: {event:?} (epxected EOF)"),
}
if expected_events.is_empty() {
break;
}
}
drop(runner);
stream.next().await.unwrap().unwrap_err();
assert_eq!(2, signal_counter);
let expected_sse_status_counter = 2 + (start_ts.elapsed().as_secs() / 3);
let diff = expected_sse_status_counter.saturating_sub(sse_status_counter);
assert!(diff <= 1 || sse_status_counter.saturating_sub(expected_sse_status_counter) <= 1);
}
type TestEvent = DatastarEvent<JsonEventData<UpdateSignals>>;
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
struct UpdateSignals {
delay: Option<u64>,
}