use std::borrow::Cow;
use autumn_web::app::AppBuilder;
use autumn_web::plugin::Plugin;
use crate::assets::MOTION_ASSETS;
pub const PLUGIN_NAME: &str = "autumn-plugin-motion";
#[derive(Debug, Default)]
#[must_use]
pub struct MotionPlugin;
impl MotionPlugin {
pub const fn new() -> Self {
Self
}
}
impl Plugin for MotionPlugin {
fn name(&self) -> Cow<'static, str> {
Cow::Borrowed(PLUGIN_NAME)
}
fn build(self, app: AppBuilder) -> AppBuilder {
app.plugin_assets(&MOTION_ASSETS)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assets::{INIT_JS, MOTION_CSS, MOTION_JS};
use autumn_web::assets::{PLUGIN_ASSETS_ROUTE_MARKER, asset_url};
use autumn_web::plugin_conformance::{ConformanceConfig, run_conformance};
use autumn_web::route_listing::{RouteClassification, RouteSource};
use autumn_web::test::{TestApp, TestClient};
const JS: &str = "text/javascript; charset=utf-8";
const CSS: &str = "text/css; charset=utf-8";
const IMMUTABLE: &str = "public, max-age=31536000, immutable";
const REVALIDATE: &str = "public, max-age=0, must-revalidate";
fn client() -> TestClient {
TestApp::new().plugin(MotionPlugin::new()).build()
}
fn url(path: &str) -> String {
MOTION_ASSETS.url(path)
}
#[tokio::test]
async fn motion_js_serves_at_its_fingerprinted_url() {
let response = client().get(&url(MOTION_JS)).send().await;
response
.assert_ok()
.assert_header("content-type", JS)
.assert_header("cache-control", IMMUTABLE);
assert!(
response.body.starts_with(b"!function"),
"serves the minified UMD build"
);
assert!(
response
.body
.windows(b"window.Motion".len())
.any(|w| w == b"window.Motion"),
"UMD build targets the Motion global"
);
}
#[tokio::test]
async fn init_js_serves_at_its_fingerprinted_url() {
let response = client().get(&url(INIT_JS)).send().await;
response
.assert_ok()
.assert_header("content-type", JS)
.assert_header("cache-control", IMMUTABLE);
let text = response.text();
assert!(
text.contains("htmx:afterSwap"),
"init script re-scans on htmx swaps"
);
assert!(
text.contains("prefers-reduced-motion"),
"init script respects reduced motion"
);
}
#[tokio::test]
async fn motion_css_serves_at_its_fingerprinted_url() {
let response = client().get(&url(MOTION_CSS)).send().await;
response
.assert_ok()
.assert_header("content-type", CSS)
.assert_header("cache-control", IMMUTABLE);
assert!(
response.text().contains(".motion-progress"),
"stylesheet styles the progress bar"
);
}
#[tokio::test]
async fn plain_urls_serve_with_revalidation_and_etags() {
let client = client();
for path in [MOTION_JS, INIT_JS, MOTION_CSS] {
let plain = format!("/static/_plugins/motion/{path}");
let response = client.get(&plain).send().await;
response
.assert_ok()
.assert_header("cache-control", REVALIDATE);
let etag = response.header("etag").expect("etag is set").to_owned();
client
.get(&plain)
.header("if-none-match", &etag)
.send()
.await
.assert_status(304);
}
}
#[tokio::test]
async fn unbundled_and_stale_paths_are_not_found() {
let client = client();
for path in [
"/static/_plugins/motion/manifest.json",
"/static/_plugins/motion/init.00000000.js",
"/static/_plugins/motion/nope.js",
"/__motion/init.js",
] {
client.get(path).send().await.assert_status(404);
}
}
#[tokio::test]
async fn asset_url_resolves_the_installed_bundle() {
let _client = client();
for path in [MOTION_JS, INIT_JS, MOTION_CSS] {
assert_eq!(asset_url(&format!("_plugins/motion/{path}")), url(path));
}
}
#[test]
fn bundle_routes_are_public_plugin_routes() {
let app = autumn_web::app().plugin(MotionPlugin::new());
let infos = app.plugin_route_infos().expect("route infos");
let asset_routes: Vec<_> = infos
.iter()
.filter(|info| info.path.starts_with("/static/_plugins/motion/"))
.collect();
assert_eq!(
asset_routes.len(),
6,
"three files, two URLs each: {infos:?}"
);
for info in asset_routes {
assert_eq!(info.method, "GET");
assert_eq!(info.classification, RouteClassification::Public);
assert_eq!(info.middleware, [PLUGIN_ASSETS_ROUTE_MARKER]);
assert_eq!(info.source, RouteSource::Plugin(PLUGIN_NAME.to_owned()));
}
}
#[test]
fn plugin_passes_conformance() {
let app = autumn_web::app().plugin(MotionPlugin::new());
let infos = app.plugin_route_infos().expect("route infos");
let report = run_conformance(&ConformanceConfig::new(PLUGIN_NAME), &infos);
assert!(report.passed(), "{}", report.to_text_report());
}
#[tokio::test]
async fn installing_the_plugin_twice_is_harmless() {
let client = TestApp::new()
.plugin(MotionPlugin::new())
.plugin(MotionPlugin::new())
.build();
client.get(&url(INIT_JS)).send().await.assert_ok();
}
}