1mod headers;
47mod htmx;
48mod middleware;
49
50pub use self::{
51 htmx::{Htmx, SwapType, TriggerType},
52 middleware::HtmxMiddleware,
53};
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::headers::ResponseHeaders;
59 use actix_web::http::header::HeaderValue;
60 use actix_web::{
61 http::header::HeaderName,
62 test::{self, TestRequest},
63 web, App, HttpResponse,
64 };
65
66 #[actix_web::test]
67 async fn test_htmx_middleware_basic() {
68 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
69 "/test",
70 web::get().to(|htmx: Htmx| async move {
71 htmx.trigger_event(
72 "test-event".to_string(),
73 Some("test-value".to_string()),
74 Some(TriggerType::Standard),
75 );
76 HttpResponse::Ok().finish()
77 }),
78 ))
79 .await;
80
81 let req = TestRequest::get()
82 .uri("/test")
83 .insert_header((HeaderName::from_static("hx-request"), "true"))
84 .to_request();
85
86 let resp = test::call_service(&app, req).await;
87 assert!(resp.status().is_success());
88
89 let trigger_header = resp
90 .headers()
91 .get(HeaderName::from_static(ResponseHeaders::HX_TRIGGER))
92 .unwrap();
93 assert!(trigger_header
94 .to_str()
95 .unwrap()
96 .contains(r#""test-event": "test-value""#));
97 }
98
99 #[actix_web::test]
100 async fn test_htmx_middleware_after_settle() {
101 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
102 "/test",
103 web::get().to(|htmx: Htmx| async move {
104 htmx.trigger_event(
105 "settle-event".to_string(),
106 None,
107 Some(TriggerType::AfterSettle),
108 );
109 HttpResponse::Ok().finish()
110 }),
111 ))
112 .await;
113
114 let req = TestRequest::get()
115 .uri("/test")
116 .insert_header((HeaderName::from_static("hx-request"), "true"))
117 .to_request();
118
119 let resp = test::call_service(&app, req).await;
120 assert!(resp.status().is_success());
121
122 let settle_header = resp
123 .headers()
124 .get(HeaderName::from_static(
125 ResponseHeaders::HX_TRIGGER_AFTER_SETTLE,
126 ))
127 .unwrap();
128
129 assert!(settle_header.to_str().unwrap().contains("settle-event"));
130 }
131
132 #[actix_web::test]
133 async fn test_htmx_request_detection() {
134 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
135 "/test",
136 web::get().to(|htmx: Htmx| async move {
137 assert!(htmx.is_htmx);
138 HttpResponse::Ok().finish()
139 }),
140 ))
141 .await;
142
143 let req = TestRequest::get()
144 .uri("/test")
145 .insert_header((HeaderName::from_static("hx-request"), "true"))
146 .to_request();
147
148 let resp = test::call_service(&app, req).await;
149 assert!(resp.status().is_success());
150 }
151
152 #[actix_web::test]
153 async fn test_non_htmx_request() {
154 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
155 "/test",
156 web::get().to(|htmx: Htmx| async move {
157 assert!(!htmx.is_htmx);
158 HttpResponse::Ok().finish()
159 }),
160 ))
161 .await;
162
163 let req = TestRequest::get().uri("/test").to_request();
164 let resp = test::call_service(&app, req).await;
165 assert!(resp.status().is_success());
166 }
167
168 #[actix_web::test]
169 async fn test_boosted_request() {
170 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
171 "/test",
172 web::get().to(|htmx: Htmx| async move {
173 assert!(htmx.boosted);
174 HttpResponse::Ok().finish()
175 }),
176 ))
177 .await;
178
179 let req = TestRequest::get()
180 .uri("/test")
181 .insert_header((HeaderName::from_static("hx-boosted"), "true"))
182 .to_request();
183
184 let resp = test::call_service(&app, req).await;
185 assert!(resp.status().is_success());
186 }
187
188 #[actix_web::test]
189 async fn test_htmx_reswap() {
190 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
191 "/test",
192 web::get().to(|htmx: Htmx| async move {
193 htmx.reswap(SwapType::Delete);
194 HttpResponse::Ok().finish()
195 }),
196 ))
197 .await;
198
199 let req = TestRequest::get()
200 .uri("/test")
201 .insert_header((HeaderName::from_static("hx-request"), "true"))
202 .to_request();
203
204 let resp = test::call_service(&app, req).await;
205 assert!(resp.status().is_success());
206
207 let reswap_header = resp
208 .headers()
209 .get(HeaderName::from_static(ResponseHeaders::HX_RESWAP))
210 .unwrap();
211
212 assert_eq!(reswap_header.to_str().unwrap(), "delete");
213 }
214
215 #[actix_web::test]
216 async fn test_multiple_triggers() {
217 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
218 "/test",
219 web::get().to(|htmx: Htmx| async move {
220 htmx.trigger_event(
221 "event1".to_string(),
222 Some("value1".to_string()),
223 Some(TriggerType::Standard),
224 );
225 htmx.trigger_event(
226 "event2".to_string(),
227 Some("value2".to_string()),
228 Some(TriggerType::Standard),
229 );
230 HttpResponse::Ok().finish()
231 }),
232 ))
233 .await;
234
235 let req = TestRequest::get()
236 .uri("/test")
237 .insert_header((HeaderName::from_static("hx-request"), "true"))
238 .to_request();
239
240 let resp = test::call_service(&app, req).await;
241 assert!(resp.status().is_success());
242
243 let trigger_header = resp
244 .headers()
245 .get(HeaderName::from_static(ResponseHeaders::HX_TRIGGER))
246 .unwrap()
247 .to_str()
248 .unwrap();
249
250 assert!(trigger_header.contains("event1"));
251 assert!(trigger_header.contains("value1"));
252 assert!(trigger_header.contains("event2"));
253 assert!(trigger_header.contains("value2"));
254 }
255
256 #[actix_web::test]
257 async fn test_multiple_trigger_types() {
258 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
259 "/test",
260 web::get().to(|htmx: Htmx| async move {
261 htmx.trigger_event(
262 "standard".to_string(),
263 Some("value1".to_string()),
264 Some(TriggerType::Standard),
265 );
266 htmx.trigger_event(
267 "after_settle".to_string(),
268 Some("value2".to_string()),
269 Some(TriggerType::AfterSettle),
270 );
271 htmx.trigger_event(
272 "after_swap".to_string(),
273 Some("value3".to_string()),
274 Some(TriggerType::AfterSwap),
275 );
276 HttpResponse::Ok().finish()
277 }),
278 ))
279 .await;
280
281 let req = TestRequest::get()
282 .uri("/test")
283 .insert_header((HeaderName::from_static("hx-request"), "true"))
284 .to_request();
285
286 let resp = test::call_service(&app, req).await;
287 assert!(resp.status().is_success());
288
289 let standard_header = resp
291 .headers()
292 .get(HeaderName::from_static(ResponseHeaders::HX_TRIGGER))
293 .unwrap()
294 .to_str()
295 .unwrap();
296 assert!(standard_header.contains("standard"));
297 assert!(standard_header.contains("value1"));
298
299 let after_settle_header = resp
301 .headers()
302 .get(HeaderName::from_static(
303 ResponseHeaders::HX_TRIGGER_AFTER_SETTLE,
304 ))
305 .unwrap()
306 .to_str()
307 .unwrap();
308 assert!(after_settle_header.contains("after_settle"));
309 assert!(after_settle_header.contains("value2"));
310
311 let after_swap_header = resp
313 .headers()
314 .get(HeaderName::from_static(
315 ResponseHeaders::HX_TRIGGER_AFTER_SWAP,
316 ))
317 .unwrap()
318 .to_str()
319 .unwrap();
320 assert!(after_swap_header.contains("after_swap"));
321 assert!(after_swap_header.contains("value3"));
322 }
323
324 #[actix_web::test]
325 async fn test_htmx_redirect() {
326 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
327 "/test",
328 web::get().to(|htmx: Htmx| async move {
329 htmx.redirect("/new-location".to_string());
330 HttpResponse::Ok().finish()
331 }),
332 ))
333 .await;
334
335 let req = TestRequest::get()
336 .uri("/test")
337 .insert_header((HeaderName::from_static("hx-request"), "true"))
338 .to_request();
339
340 let resp = test::call_service(&app, req).await;
341 assert!(resp.status().is_success());
342
343 let redirect_header = resp
344 .headers()
345 .get(HeaderName::from_static(ResponseHeaders::HX_REDIRECT))
346 .unwrap();
347
348 assert_eq!(redirect_header.to_str().unwrap(), "/new-location");
349 }
350
351 #[actix_web::test]
352 async fn test_htmx_redirect_with_swap() {
353 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
354 "/test",
355 web::get().to(|htmx: Htmx| async move {
356 htmx.redirect_with_swap("/new-location".to_string());
357 HttpResponse::Ok().finish()
358 }),
359 ))
360 .await;
361
362 let req = TestRequest::get()
363 .uri("/test")
364 .insert_header((HeaderName::from_static("hx-request"), "true"))
365 .to_request();
366
367 let resp = test::call_service(&app, req).await;
368 assert!(resp.status().is_success());
369
370 let location_header = resp
371 .headers()
372 .get(HeaderName::from_static(ResponseHeaders::HX_LOCATION))
373 .unwrap();
374
375 assert_eq!(location_header.to_str().unwrap(), "/new-location");
376 }
377
378 #[actix_web::test]
379 async fn test_url_methods() {
380 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
381 "/test",
382 web::get().to(|htmx: Htmx| async move {
383 htmx.push_url("/pushed-url".to_string());
384 htmx.replace_url("/replaced-url".to_string());
385 HttpResponse::Ok().finish()
386 }),
387 ))
388 .await;
389
390 let req = TestRequest::get()
391 .uri("/test")
392 .insert_header((HeaderName::from_static("hx-request"), "true"))
393 .to_request();
394
395 let resp = test::call_service(&app, req).await;
396 assert!(resp.status().is_success());
397
398 let push_url = resp
399 .headers()
400 .get(HeaderName::from_static(ResponseHeaders::HX_PUSH_URL))
401 .unwrap();
402 assert_eq!(push_url.to_str().unwrap(), "/pushed-url");
403
404 let replace_url = resp
405 .headers()
406 .get(HeaderName::from_static(ResponseHeaders::HX_REPLACE_URL))
407 .unwrap();
408 assert_eq!(replace_url.to_str().unwrap(), "/replaced-url");
409 }
410
411 #[actix_web::test]
412 async fn test_target_methods() {
413 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
414 "/test",
415 web::get().to(|htmx: Htmx| async move {
416 htmx.retarget("#new-target".to_string());
417 htmx.reselect("#new-selection".to_string());
418 HttpResponse::Ok().finish()
419 }),
420 ))
421 .await;
422
423 let req = TestRequest::get()
424 .uri("/test")
425 .insert_header((HeaderName::from_static("hx-request"), "true"))
426 .to_request();
427
428 let resp = test::call_service(&app, req).await;
429 assert!(resp.status().is_success());
430
431 let retarget = resp
432 .headers()
433 .get(HeaderName::from_static(ResponseHeaders::HX_RETARGET))
434 .unwrap();
435 assert_eq!(retarget.to_str().unwrap(), "#new-target");
436
437 let reselect = resp
438 .headers()
439 .get(HeaderName::from_static(ResponseHeaders::HX_RESELECT))
440 .unwrap();
441 assert_eq!(reselect.to_str().unwrap(), "#new-selection");
442 }
443
444 #[actix_web::test]
445 async fn test_request_information() {
446 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
447 "/test",
448 web::get().to(|htmx: Htmx| async move {
449 assert_eq!(htmx.current_url().unwrap(), "http://example.com");
450 assert_eq!(htmx.prompt().unwrap(), "test prompt");
451 assert_eq!(htmx.target().unwrap(), "#target");
452 assert_eq!(htmx.trigger().unwrap(), "click");
453 assert_eq!(htmx.trigger_name().unwrap(), "button1");
454 HttpResponse::Ok().finish()
455 }),
456 ))
457 .await;
458
459 let req = TestRequest::get()
460 .uri("/test")
461 .insert_header((HeaderName::from_static("hx-request"), "true"))
462 .insert_header((
463 HeaderName::from_static("hx-current-url"),
464 "http://example.com",
465 ))
466 .insert_header((HeaderName::from_static("hx-prompt"), "test prompt"))
467 .insert_header((HeaderName::from_static("hx-target"), "#target"))
468 .insert_header((HeaderName::from_static("hx-trigger"), "click"))
469 .insert_header((HeaderName::from_static("hx-trigger-name"), "button1"))
470 .to_request();
471
472 let resp = test::call_service(&app, req).await;
473 assert!(resp.status().is_success());
474 }
475
476 #[actix_web::test]
477 async fn test_refresh() {
478 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
479 "/test",
480 web::get().to(|htmx: Htmx| async move {
481 htmx.refresh();
482 HttpResponse::Ok().finish()
483 }),
484 ))
485 .await;
486
487 let req = TestRequest::get()
488 .uri("/test")
489 .insert_header((HeaderName::from_static("hx-request"), "true"))
490 .to_request();
491
492 let resp = test::call_service(&app, req).await;
493 assert!(resp.status().is_success());
494
495 let refresh = resp
496 .headers()
497 .get(HeaderName::from_static(ResponseHeaders::HX_REFRESH))
498 .unwrap();
499 assert_eq!(refresh.to_str().unwrap(), "true");
500 }
501
502 #[actix_web::test]
503 async fn test_malformed_headers() {
504 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
505 "/test",
506 web::get().to(|htmx: Htmx| async move {
507 assert_eq!(htmx.current_url(), None);
509 assert_eq!(htmx.prompt(), None);
510 assert_eq!(htmx.target(), None);
511 assert_eq!(htmx.is_htmx, false);
513 HttpResponse::Ok().finish()
514 }),
515 ))
516 .await;
517
518 let req = TestRequest::get()
519 .uri("/test")
520 .insert_header((
521 HeaderName::from_static("hx-current-url"),
522 HeaderValue::from_bytes(b"\xFF\xFF").unwrap(),
523 ))
524 .insert_header((
525 HeaderName::from_static("hx-prompt"),
526 HeaderValue::from_bytes(b"\xFF\xFF").unwrap(),
527 ))
528 .insert_header((
529 HeaderName::from_static("hx-target"),
530 HeaderValue::from_bytes(b"\xFF\xFF").unwrap(),
531 ))
532 .insert_header((
533 HeaderName::from_static("hx-request"),
534 HeaderValue::from_bytes(b"\xFF\xFF").unwrap(),
535 ))
536 .to_request();
537
538 let resp = test::call_service(&app, req).await;
539 assert!(resp.status().is_success());
540 }
541
542 #[actix_web::test]
543 async fn test_from_request_with_extensions() {
544 let app = test::init_service(App::new().wrap(HtmxMiddleware).route(
545 "/test",
546 web::get().to(|htmx1: Htmx, htmx2: Htmx| async move {
547 assert_eq!(htmx1.is_htmx, htmx2.is_htmx);
549 assert_eq!(htmx1.boosted, htmx2.boosted);
550 HttpResponse::Ok().finish()
551 }),
552 ))
553 .await;
554
555 let req = TestRequest::get()
556 .uri("/test")
557 .insert_header((HeaderName::from_static("hx-request"), "true"))
558 .to_request();
559
560 let resp = test::call_service(&app, req).await;
561 assert!(resp.status().is_success());
562 }
563}