use axum::http::{header, HeaderMap}; use axum::response::IntoResponse; use scraper::{Html, Selector}; use slhx_axum::{ interactions, runtime_js, DispatchRejection, EffectResponse, InteractionForm, InteractionFormRejection, InteractionRequest, PageMode, PageRequest, PageResponse, SLHX_CONTENT_TYPE, SLHX_FINGERPRINT_HEADER, SLHX_PARTIAL_HEADER, SLHX_RUNTIME_CONTENT_TYPE, SLHX_TITLE_HEADER, }; use slhx_core::{push, BuildFingerprint, Handle, SafeHtml, Slot}; fn selector(value: &str) -> Selector { Selector::parse(value).expect("test selector parses") } #[test] fn page_mode_detects_partial_header() { let mut headers = HeaderMap::new(); assert_eq!(PageMode::from_headers(&headers), PageMode::Full); headers.insert(SLHX_PARTIAL_HEADER, "true".parse().unwrap()); assert_eq!(PageMode::from_headers(&headers), PageMode::Partial); } #[test] fn page_request_wraps_full_pages_and_leaves_partials_unwrapped() { // req: test/005 let full = PageRequest { mode: PageMode::Full }.page( "
Docs
", |content| format!("{content}"), ); assert_eq!(full.mode, PageMode::Full); let full_document = Html::parse_document(&full.html); assert_eq!( full_document .select(&selector("body[data-shell=\"docs\"] main[data-page=\"docs\"]")) .count(), 1 ); let partial = PageRequest { mode: PageMode::Partial }.page( "
Docs
", |content| format!("{content}"), ); assert_eq!(partial.mode, PageMode::Partial); let partial_fragment = Html::parse_fragment(&partial.html); assert_eq!(partial_fragment.select(&selector("main[data-page=\"docs\"]")).count(), 1); assert_eq!(partial_fragment.select(&selector("body[data-shell=\"docs\"]")).count(), 0); } #[test] fn page_request_wraps_safe_html_full_pages_and_leaves_partials_unwrapped() { // req: axum_integration/001 req: html_safety/001 req: html_safety/002 req: test/005 let full = PageRequest { mode: PageMode::Full, } .page_html( SafeHtml::trusted("
Docs
"), |content| { SafeHtml::trusted(format!( "{content}" )) }, ); assert_eq!(full.mode, PageMode::Full); let full_document = Html::parse_document(&full.html); assert_eq!( full_document .select(&selector("body[data-shell=\"docs\"] main[data-page=\"docs\"]")) .count(), 1 ); let partial = PageRequest { mode: PageMode::Partial, } .page_html( SafeHtml::trusted("
Docs
"), |content| { SafeHtml::trusted(format!( "{content}" )) }, ); assert_eq!(partial.mode, PageMode::Partial); let partial_fragment = Html::parse_fragment(&partial.html); assert_eq!(partial_fragment.select(&selector("main[data-page=\"docs\"]")).count(), 1); assert_eq!(partial_fragment.select(&selector("body[data-shell=\"docs\"]")).count(), 0); } #[test] fn partial_page_response_sets_partial_and_title_headers() { let response = PageResponse::partial("
Docs
") .title("Docs") .into_response(); assert_eq!(response.headers()[header::CONTENT_TYPE], "text/html; charset=utf-8"); assert_eq!(response.headers()[SLHX_PARTIAL_HEADER], "true"); assert_eq!(response.headers()[SLHX_TITLE_HEADER], "Docs"); } #[test] fn effect_response_is_wire_batch_with_fingerprint_header() { let response = EffectResponse::new(push("/docs"), BuildFingerprint(99)).into_response(); assert_eq!(response.headers()[header::CONTENT_TYPE], SLHX_CONTENT_TYPE); assert_eq!(response.headers()[SLHX_FINGERPRINT_HEADER], "99"); } #[test] fn interaction_form_parses_handle_and_fields() { let form = InteractionForm::parse_urlencoded(b"__h=42&title=Hello+World&tag=a&tag=b%2Fc") .unwrap(); assert_eq!(form.handle_id, 42); assert_eq!(form.value("title"), Some("Hello World")); assert_eq!(form.values("tag").collect::>(), ["a", "b/c"]); } #[test] fn interaction_form_parses_typed_values() { // req: form/004 req: dx/003 let form = InteractionForm::parse_urlencoded(b"__h=42&count=7&bad=nope") .expect("form should parse"); assert_eq!(form.parse::("count"), Some(7)); assert_eq!(form.parse::("bad"), None); assert_eq!(form.parse::("missing"), None); } #[test] fn interaction_form_requires_numeric_handle() { assert_eq!( InteractionForm::parse_urlencoded(b"title=Hello").unwrap_err(), InteractionFormRejection::MissingHandle ); assert_eq!( InteractionForm::parse_urlencoded(b"__h=nope").unwrap_err(), InteractionFormRejection::InvalidHandle ); } #[test] fn interaction_form_preserves_hidden_csrf_fields_for_extractors() { // req: auth/004 let form = InteractionForm::parse_urlencoded(b"__h=42&csrf_token=abc123&title=Hello") .unwrap(); assert_eq!(form.handle_id, 42); assert_eq!(form.value("csrf_token"), Some("abc123")); assert!(form.fields().iter().any(|(name, _)| name == "csrf_token")); } #[test] fn interaction_request_dispatches_with_concise_handlers_helper() { // req: axum_integration/003 req: ceremony/001 req: dx/003 let request = InteractionRequest::from(InteractionForm::for_handle( Handle::<()>::new(7), Vec::new(), )); let response = request .dispatch(interactions(BuildFingerprint(4)).on(Handle::<()>::new(7), |_| { Slot::::new(3).text("ok") })) .unwrap(); assert_eq!(response.batch.fingerprint, BuildFingerprint(4)); assert_eq!(response.batch.ops, vec![Slot::::new(3).text("ok")]); } #[test] fn interactions_dispatch_by_checked_handle() { // req: ceremony/004 req: public_api/001 let title = Slot::::new(7); let create = Handle::<()>::new(42); let request = InteractionRequest::from(InteractionForm::for_handle( create, [("title".into(), "Hello".into())], )); let response = request .dispatch(interactions(BuildFingerprint(123)).on(create, move |form| { title.text(form.value("title").unwrap_or("")) })) .unwrap(); assert_eq!(response.batch.fingerprint, BuildFingerprint(123)); assert_eq!(response.batch.ops, vec![title.text("Hello")]); } #[test] fn interactions_reject_unknown_handle_ids() { let request = InteractionRequest::from(InteractionForm::new(9, [])); assert_eq!( request.dispatch(interactions(BuildFingerprint(123))).unwrap_err(), DispatchRejection::UnknownHandle(9) ); } #[test] fn runtime_js_response_serves_embedded_runtime() { let response = runtime_js().into_response(); assert_eq!(response.headers()[header::CONTENT_TYPE], SLHX_RUNTIME_CONTENT_TYPE); assert!(response.headers().contains_key(header::CACHE_CONTROL)); }