Skip to main content

ferridriver_test/ct/
server.rs

1//! ComponentServer: embedded HTTP server for component testing.
2//!
3//! Serves static files (HTML, JS, WASM, CSS) from a directory on a random port.
4//! Used by both WASM and Vite paths — the WASM path serves compiled output directly,
5//! the Vite path uses this as a fallback or the Vite dev server directly.
6
7use std::net::SocketAddr;
8use std::path::Path;
9
10use axum::Router;
11use tower_http::services::ServeDir;
12
13/// A lightweight HTTP server for serving component test assets.
14pub struct ComponentServer {
15  addr: SocketAddr,
16  shutdown_tx: tokio::sync::oneshot::Sender<()>,
17  handle: tokio::task::JoinHandle<()>,
18}
19
20impl ComponentServer {
21  /// Start serving files from `root_dir` on a random available port.
22  ///
23  /// # Errors
24  ///
25  /// Returns an error if the server fails to bind.
26  pub async fn start(root_dir: &Path) -> ferridriver::error::Result<Self> {
27    let service = ServeDir::new(root_dir).append_index_html_on_directories(true);
28
29    let app = Router::new().fallback_service(service);
30
31    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
32
33    let addr = listener.local_addr()?;
34
35    let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
36
37    let handle = tokio::spawn(async move {
38      axum::serve(listener, app)
39        .with_graceful_shutdown(async {
40          let _ = shutdown_rx.await;
41        })
42        .await
43        .ok();
44    });
45
46    Ok(Self {
47      addr,
48      shutdown_tx,
49      handle,
50    })
51  }
52
53  /// The base URL of the server (e.g. `http://127.0.0.1:39201`).
54  #[must_use]
55  pub fn url(&self) -> String {
56    format!("http://{}", self.addr)
57  }
58
59  /// The port the server is listening on.
60  #[must_use]
61  pub fn port(&self) -> u16 {
62    self.addr.port()
63  }
64
65  /// Stop the server.
66  pub async fn stop(self) {
67    let _ = self.shutdown_tx.send(());
68    let _ = self.handle.await;
69  }
70}
71
72/// The minimal HTML wrapper that loads a WASM component.
73/// Sets `data-mounted="true"` on body after WASM init completes.
74pub fn wasm_html_wrapper(wasm_js_path: &str) -> String {
75  format!(
76    r#"<!DOCTYPE html>
77<html>
78<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
79<body>
80<div id="app"></div>
81<script type="module">
82import init from './{wasm_js_path}';
83await init();
84document.body.setAttribute('data-mounted', 'true');
85</script>
86</body>
87</html>"#
88  )
89}
90
91/// The minimal HTML wrapper that loads a JS component (from Vite).
92/// The Vite dev server handles HMR and bundling; this just provides the shell.
93pub fn vite_html_wrapper(entry_path: &str) -> String {
94  format!(
95    r#"<!DOCTYPE html>
96<html>
97<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
98<body>
99<div id="app"></div>
100<script type="module" src="{entry_path}"></script>
101</body>
102</html>"#
103  )
104}