pub mod css_module;
pub(crate) mod html_encode;
pub mod plugin;
mod render_capacity;
mod response_writer;
pub mod route_handler;
pub mod route_matcher;
pub(crate) mod route_renderer;
pub(crate) mod streaming;
#[doc(hidden)]
pub use render_capacity::RenderCapacityHints;
#[doc(hidden)]
pub use response_writer::{
append_attribute_to_bytes, append_attribute_to_string, append_boolean_attribute_to_bytes,
append_boolean_attribute_to_string,
};
pub use route_handler::Protocol;
pub use html_encode::encode_safe;
use plugin::BootstrapExtensionContext;
use plugin::HandlerPlugin;
use plugin::WebUiTemplatePayload;
use route_handler::ComponentReachabilityIndex;
use route_matcher::CompiledRouteIndex;
use serde::ser::SerializeMap;
use serde::Serialize;
use serde_json::Value;
use std::borrow::Cow;
use std::cell::{Cell, OnceCell};
use std::collections::{HashMap, HashSet};
use std::fmt::{self, Write as _};
use std::hash::{BuildHasherDefault, Hasher};
use std::sync::{Arc, LazyLock};
use streaming::{
consume_streaming_component_root, ensure_no_pending_streaming_root,
prepare_generated_streaming_root, record_checkpoint_tag, streaming_template_already_sent,
validate_pending_streaming_root, validate_streaming_root_opening, ComponentHostOrigin,
StreamingRenderState,
};
pub use streaming::{
BoundaryDescriptor, BoundaryInstanceId, BoundaryKey, BoundaryMode, BufferSink, SessionOptions,
SpanInstanceId, StreamStatus, StreamStep, StreamingResponse, StreamingSession, StreamingState,
MAX_BOUNDARY_OCCURRENCES, MAX_CONTINUATION_DEPTH, MAX_KEYED_INSTANCES, MAX_SPAN_NESTING,
};
use thiserror::Error;
use webui_expressions::{evaluate_with_resolver, ExpressionError};
use webui_protocol::{
web_ui_fragment::Fragment, ComponentAssetStylePreload, ComponentWorkPolicy, FragmentList,
InitialStateStrategy, StateProjectionMode, WebUIFragment, WebUIProtocol,
};
use webui_state::find_value_by_dotted_path_ref;
#[derive(Debug, Error)]
pub enum HandlerError {
#[error("Rendering error: {0}")]
Rendering(String),
#[error("invalid state JSON: {0}")]
InvalidState(String),
#[error("Rendering invariant error: {0}")]
Invariant(String),
#[error("Missing fragment: {0}")]
MissingFragment(String),
#[error("Missing data field: {0}")]
MissingData(String),
#[error("Type error: {0}")]
TypeError(String),
#[error("Protocol error: {0}")]
Protocol(#[from] webui_protocol::ProtocolError),
#[error("Evaluation error: {0}")]
Evaluation(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Writer error: {0}")]
Writer(String),
#[error("Plugin data error: {0}")]
PluginData(String),
#[error("client disconnected")]
ClientDisconnected,
#[error("streaming flush timed out")]
StreamTimeout,
#[error("invalid streaming boundary signal `{}`: {}", .0.signal, .0.reason)]
StreamingBoundary(Box<StreamingBoundaryError>),
#[error("streaming render ended before `body_end`; no terminal boundary record was emitted")]
MissingStreamingBodyEnd,
#[error(
"streaming protocol is missing the required `head_start` signal before `{before}`; \
rebuild the protocol with streaming-boundary parser support"
)]
MissingStreamingHeadStart {
before: &'static str,
},
#[error("streaming protocol emitted duplicate `head_start` signals")]
DuplicateStreamingHeadStart,
}
#[derive(Debug)]
pub struct StreamingBoundaryError {
pub signal: String,
pub reason: String,
}
pub type Result<T> = std::result::Result<T, HandlerError>;
#[cold]
#[inline(never)]
fn invalid_fragment_range_error(
range: &std::ops::Range<usize>,
fragment_count: usize,
) -> HandlerError {
HandlerError::Invariant(format!(
"fragment range {}..{} exceeds fragment count {fragment_count}",
range.start, range.end
))
}
#[cold]
#[inline(never)]
fn route_style_plan_missing_error() -> HandlerError {
HandlerError::Invariant(
"matched route style targets were computed without a route chain".to_string(),
)
}
#[cold]
#[inline(never)]
fn route_style_plan_length_error(routes: usize, targets: usize) -> HandlerError {
HandlerError::Invariant(format!(
"matched route style target count {targets} does not match route count {routes}"
))
}
pub trait ResponseWriter {
fn write(&mut self, content: &str) -> Result<()>;
#[doc(hidden)]
fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
self.write(" ")?;
self.write(name)?;
self.write("=\"")?;
self.write(value)?;
self.write("\"")
}
#[doc(hidden)]
fn write_boolean_attribute(&mut self, name: &str) -> Result<()> {
self.write(" ")?;
self.write(name)
}
fn end(&mut self) -> Result<()>;
#[doc(hidden)]
fn stream_flush(&mut self) -> Result<()> {
Ok(())
}
#[doc(hidden)]
fn stream_begin_component(&mut self) -> Result<()> {
Err(HandlerError::Invariant(
"component opening buffering requires a streaming sink".to_string(),
))
}
#[doc(hidden)]
fn stream_mark_component_root(&mut self) -> Result<()> {
Err(HandlerError::Invariant(
"component root buffering requires a streaming sink".to_string(),
))
}
#[doc(hidden)]
fn stream_commit_component(
&mut self,
span_id: Option<u32>,
enclosing_span_id: Option<u32>,
deferred: bool,
) -> Result<()> {
let _ = (span_id, enclosing_span_id, deferred);
Err(HandlerError::Invariant(
"component opening commit requires a streaming sink".to_string(),
))
}
}
struct StateWithoutSelectedKeys<'a> {
value: &'a Value,
keys: KeyView<'a>,
}
impl Serialize for StateWithoutSelectedKeys<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let Value::Object(map) = self.value else {
return serializer.serialize_map(Some(0))?.end();
};
let mut out = serializer.serialize_map(None)?;
for (key, value) in map {
if key != STATE_INJECT_KEY && !self.keys.contains(key) {
out.serialize_entry(key, value)?;
}
}
out.end()
}
}
pub trait FlushWriter: ResponseWriter {
fn flush(&mut self) -> Result<()>;
}
pub struct RenderOptions<'a> {
pub entry_id: &'a str,
pub request_path: &'a str,
pub nonce: Option<&'a str>,
pub head_inject: Option<&'a str>,
pub body_inject: Option<&'a str>,
}
pub const STATE_INJECT_KEY: &str = "$webui";
#[derive(Clone, Copy, Default)]
pub(crate) struct StateInject<'state> {
pub(crate) head_end: Option<&'state str>,
pub(crate) body_start: Option<&'state str>,
pub(crate) body_end: Option<&'state str>,
}
impl<'state> StateInject<'state> {
pub(crate) fn resolve(state: &'state Value) -> Self {
let Some(Value::Object(map)) = state.get(STATE_INJECT_KEY) else {
return Self::default();
};
let field = |name: &str| {
map.get(name)
.and_then(Value::as_str)
.filter(|html| !html.is_empty())
};
Self {
head_end: field("headEnd"),
body_start: field("bodyStart"),
body_end: field("bodyEnd"),
}
}
}
impl<'a> RenderOptions<'a> {
#[must_use]
pub fn new(entry_id: &'a str, request_path: &'a str) -> Self {
Self {
entry_id,
request_path,
nonce: None,
head_inject: None,
body_inject: None,
}
}
#[must_use]
pub fn with_nonce(mut self, nonce: &'a str) -> Self {
self.nonce = if nonce.is_empty() { None } else { Some(nonce) };
self
}
#[must_use]
pub fn with_head_inject(mut self, html: &'a str) -> Self {
self.head_inject = if html.is_empty() { None } else { Some(html) };
self
}
#[must_use]
pub fn with_body_inject(mut self, html: &'a str) -> Self {
self.body_inject = if html.is_empty() { None } else { Some(html) };
self
}
}
pub struct WebUIHandler {
plugin_factory: Option<fn() -> Box<dyn HandlerPlugin>>,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum StyleClosureInstall {
Static,
Routed,
}
pub(crate) struct ShadowStyleRoot {
component_index: u32,
static_closure_emitted: bool,
routed_resources: Vec<u32>,
}
struct LoopBinding<'protocol, 'state> {
name: &'protocol str,
value: &'state Value,
}
#[derive(Clone, Copy)]
struct VisibleLoopScope {
start: usize,
end: usize,
}
impl VisibleLoopScope {
const EMPTY: Self = Self { start: 0, end: 0 };
}
pub(crate) type ScopeMap = HashMap<String, Value, BuildHasherDefault<ScopeKeyHasher>>;
#[derive(Clone, Copy, Default)]
pub(crate) struct ScopeKeyHasher(u64);
impl ScopeKeyHasher {
const SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
#[inline]
fn add_word(&mut self, word: u64) {
self.0 = (self.0.rotate_left(5) ^ word).wrapping_mul(Self::SEED);
}
}
impl Hasher for ScopeKeyHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
let (words, tail) = bytes.as_chunks::<8>();
for word in words {
self.add_word(u64::from_le_bytes(*word));
}
if !tail.is_empty() {
let mut word = [0u8; 8];
word[..tail.len()].copy_from_slice(tail);
self.add_word(u64::from_le_bytes(word));
}
}
#[inline]
fn write_u8(&mut self, byte: u8) {
self.add_word(u64::from(byte));
}
#[inline]
fn finish(&self) -> u64 {
self.0
}
}
#[derive(Clone, Copy)]
struct LocalValueSources<'ctx, 'protocol, 'state> {
owned: &'ctx ScopeMap,
borrowed: &'ctx BorrowedScope<'protocol, 'state>,
}
pub(crate) type RouteChildren<'protocol> = Cow<'protocol, [webui_protocol::WebUiFragmentRoute]>;
#[derive(Default)]
struct BorrowedScope<'protocol, 'state> {
inline: [Option<(&'protocol str, &'state Value)>; INLINE_SCOPE_SLOTS],
inline_len: usize,
overflow: Vec<(&'protocol str, &'state Value)>,
}
impl<'protocol, 'state> BorrowedScope<'protocol, 'state> {
fn get(&self, name: &str) -> Option<&'state Value> {
if self.inline_len == 0 {
return None;
}
if let Some((entry_name, value)) = self.inline[0].as_ref() {
if *entry_name == name {
return Some(*value);
}
}
for (entry_name, value) in self.inline[1..self.inline_len].iter().flatten() {
if *entry_name == name {
return Some(*value);
}
}
self.overflow
.iter()
.find_map(|(entry_name, value)| (*entry_name == name).then_some(*value))
}
fn insert(&mut self, name: &'protocol str, value: &'state Value) -> Option<&'state Value> {
for (entry_name, current) in self.inline[..self.inline_len].iter_mut().flatten() {
if *entry_name == name {
return Some(std::mem::replace(current, value));
}
}
for (entry_name, current) in &mut self.overflow {
if *entry_name == name {
return Some(std::mem::replace(current, value));
}
}
if self.inline_len < INLINE_SCOPE_SLOTS {
self.inline[self.inline_len] = Some((name, value));
self.inline_len += 1;
} else {
self.overflow.push((name, value));
}
None
}
fn remove(&mut self, name: &str) -> Option<&'state Value> {
if let Some(index) = self.inline[..self.inline_len].iter().position(|entry| {
entry
.as_ref()
.is_some_and(|(entry_name, _)| *entry_name == name)
}) {
let removed = self.inline[index].take().map(|(_, value)| value);
if let Some(entry) = self.overflow.pop() {
self.inline[index] = Some(entry);
} else {
self.inline_len -= 1;
self.inline[index] = self.inline[self.inline_len].take();
}
return removed;
}
let index = self
.overflow
.iter()
.position(|(entry_name, _)| *entry_name == name)?;
Some(self.overflow.swap_remove(index).1)
}
fn clear(&mut self) {
for entry in &mut self.inline[..self.inline_len] {
*entry = None;
}
self.inline_len = 0;
self.overflow.clear();
}
fn clone_into_owned(&self, target: &mut ScopeMap) {
for (name, value) in self.inline[..self.inline_len].iter().flatten() {
target.insert((*name).to_owned(), (*value).clone());
}
for (name, value) in &self.overflow {
target.insert((*name).to_owned(), (*value).clone());
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct RenderFragmentList<'protocol> {
fragments: &'protocol [WebUIFragment],
metadata: &'protocol [RenderFragmentMetadata],
attr_names: &'protocol str,
contains_boundary: bool,
has_routes: bool,
}
impl<'protocol> RenderFragmentList<'protocol> {
fn target(self, index: usize) -> Option<usize> {
let target = self.metadata.get(index)?.target;
(target != NO_RENDER_SLOT).then_some(target as usize)
}
fn component_attr_name(self, index: usize) -> Option<&'protocol str> {
let prepared = self.metadata.get(index)?;
if prepared.attr_start == NO_ATTR_NAME {
return None;
}
let start = prepared.attr_start as usize;
self.attr_names
.get(start..start + prepared.attr_len as usize)
}
}
const NO_RENDER_SLOT: u32 = u32::MAX;
const NO_ATTR_NAME: u32 = u32::MAX;
#[derive(Clone, Copy)]
struct RenderFragmentMetadata {
target: u32,
attr_start: u32,
attr_len: u32,
}
pub(crate) struct RenderFragmentIndex {
ids: Box<[Arc<str>]>,
metadata: Box<[RenderFragmentMetadata]>,
ranges: Box<[u32]>,
attr_names: Box<str>,
route_presence: Box<[u64]>,
}
pub(crate) struct ResolvedRenderFragmentIndex<'protocol> {
cache: [Cell<Option<&'protocol FragmentList>>; INLINE_RENDER_FRAGMENT_LISTS],
spilled: OnceCell<Box<[Cell<Option<&'protocol FragmentList>>]>>,
index: &'protocol RenderFragmentIndex,
protocol: &'protocol WebUIProtocol,
}
impl RenderFragmentIndex {
pub(crate) fn new(
protocol: &WebUIProtocol,
ids: &[Arc<str>],
slots: &HashMap<Arc<str>, u32>,
) -> Self {
let total_fragments = protocol
.fragments
.values()
.map(|list| list.fragments.len())
.sum();
let mut metadata = Vec::with_capacity(total_fragments);
let mut ranges = Vec::with_capacity(ids.len() + 1);
let mut attr_names = String::with_capacity(total_fragments * 8);
let mut route_presence = vec![0u64; ids.len().div_ceil(64)];
for (slot, id) in ids.iter().enumerate() {
#[allow(clippy::cast_possible_truncation)]
ranges.push(metadata.len() as u32);
let Some(list) = protocol.fragments.get(id.as_ref()) else {
continue;
};
let mut has_routes = false;
for fragment in &list.fragments {
let inner = fragment.fragment.as_ref();
if matches!(inner, Some(Fragment::Route(_))) {
has_routes = true;
}
let target = fragment_target_id(fragment)
.and_then(|target| slots.get(target).copied())
.unwrap_or(NO_RENDER_SLOT);
let (attr_start, attr_len) = match inner {
Some(Fragment::Attribute(attribute)) if !attribute.attr_skip => {
#[allow(clippy::cast_possible_truncation)]
let start = attr_names.len() as u32;
let before = attr_names.len();
push_component_attr_name(&mut attr_names, component_attr_source(attribute));
#[allow(clippy::cast_possible_truncation)]
let len = (attr_names.len() - before) as u32;
(start, len)
}
_ => (NO_ATTR_NAME, 0),
};
metadata.push(RenderFragmentMetadata {
target,
attr_start,
attr_len,
});
}
if has_routes {
route_presence[slot / 64] |= 1u64 << (slot % 64);
}
}
#[allow(clippy::cast_possible_truncation)]
ranges.push(metadata.len() as u32);
Self {
ids: ids.to_vec().into_boxed_slice(),
metadata: metadata.into_boxed_slice(),
ranges: ranges.into_boxed_slice(),
attr_names: attr_names.into_boxed_str(),
route_presence: route_presence.into_boxed_slice(),
}
}
pub(crate) fn index(&self, id: &str) -> Option<usize> {
self.ids
.binary_search_by(|candidate| candidate.as_ref().cmp(id))
.ok()
}
pub(crate) fn resolve<'protocol>(
&'protocol self,
protocol: &'protocol WebUIProtocol,
) -> ResolvedRenderFragmentIndex<'protocol> {
ResolvedRenderFragmentIndex {
cache: std::array::from_fn(|_| Cell::new(None)),
spilled: OnceCell::new(),
index: self,
protocol,
}
}
}
impl<'protocol> ResolvedRenderFragmentIndex<'protocol> {
pub(crate) fn index(&self, id: &str) -> Option<usize> {
self.index.index(id)
}
fn slot(&self, index: usize) -> Option<&Cell<Option<&'protocol FragmentList>>> {
if index < INLINE_RENDER_FRAGMENT_LISTS {
return self.cache.get(index);
}
let spilled = self.spilled.get_or_init(|| {
let overflow = self
.index
.ids
.len()
.saturating_sub(INLINE_RENDER_FRAGMENT_LISTS);
(0..overflow).map(|_| Cell::new(None)).collect()
});
spilled.get(index - INLINE_RENDER_FRAGMENT_LISTS)
}
pub(crate) fn list(&self, index: usize) -> Option<RenderFragmentList<'protocol>> {
let id = self.index.ids.get(index)?;
let fragment_list = match self.slot(index) {
Some(slot) => match slot.get() {
Some(cached) => cached,
None => {
let resolved = self.protocol.fragments.get(id.as_ref())?;
slot.set(Some(resolved));
resolved
}
},
None => self.protocol.fragments.get(id.as_ref())?,
};
let start = *self.index.ranges.get(index)? as usize;
let end = *self.index.ranges.get(index + 1)? as usize;
let metadata = self.index.metadata.get(start..end)?;
let has_routes = self
.index
.route_presence
.get(index / 64)
.is_some_and(|word| word & (1u64 << (index % 64)) != 0);
Some(RenderFragmentList {
fragments: &fragment_list.fragments,
metadata,
attr_names: &self.index.attr_names,
contains_boundary: fragment_list.contains_boundary,
has_routes,
})
}
#[cfg(test)]
pub(crate) fn list_by_id(&self, id: &str) -> Option<RenderFragmentList<'protocol>> {
self.index(id).and_then(|index| self.list(index))
}
}
fn component_attr_source(attribute: &webui_protocol::WebUIFragmentAttribute) -> &str {
let takes_complex_branch = attribute.condition_tree.is_none()
&& attribute.template.is_empty()
&& !attribute.value.is_empty()
&& !attribute.raw_value
&& attribute.complex;
if takes_complex_branch {
attribute.name.strip_prefix(':').unwrap_or(&attribute.name)
} else {
&attribute.name
}
}
fn descend_into<'protocol>(
children: &mut RouteChildren<'protocol>,
index: usize,
) -> RouteChildren<'protocol> {
match children {
Cow::Borrowed(routes) => match routes.get(index) {
Some(route) => Cow::Borrowed(&route.children),
None => Cow::Borrowed(&[]),
},
Cow::Owned(routes) => match routes.get_mut(index) {
Some(route) => Cow::Owned(std::mem::take(&mut route.children)),
None => Cow::Borrowed(&[]),
},
}
}
fn fragment_target_id(fragment: &WebUIFragment) -> Option<&str> {
match fragment.fragment.as_ref()? {
Fragment::Component(component) => Some(&component.fragment_id),
Fragment::ForLoop(for_loop) => Some(&for_loop.fragment_id),
Fragment::IfCond(if_cond) => Some(&if_cond.fragment_id),
Fragment::Attribute(attribute) if !attribute.template.is_empty() => {
Some(&attribute.template)
}
_ => None,
}
}
pub(crate) struct WebUIProcessContext<'protocol, 'state, 'output> {
pub(crate) protocol: &'protocol WebUIProtocol,
pub(crate) render_fragments: ResolvedRenderFragmentIndex<'protocol>,
pub(crate) component_asset_style_manifest: &'protocol str,
pub(crate) component_asset_style_links: &'protocol str,
pub(crate) state: &'state Value,
pub(crate) writer: &'output mut dyn ResponseWriter,
pub(crate) local_vars: ScopeMap,
local_borrowed_vars: BorrowedScope<'protocol, 'state>,
loop_vars: Vec<LoopBinding<'protocol, 'state>>,
visible_loop_scope: VisibleLoopScope,
pub(crate) component_attrs: ScopeMap,
component_borrowed_attrs: BorrowedScope<'protocol, 'state>,
collecting_component_attrs: bool,
pub(crate) request_path: &'protocol str,
pub(crate) route_base: Cow<'protocol, str>,
pub(crate) rendered_components: HashSet<String>,
pub(crate) plugin: Option<Box<dyn HandlerPlugin>>,
pub(crate) route_children: RouteChildren<'protocol>,
pub(crate) entry_id: &'protocol str,
pub(crate) nonce: Option<&'protocol str>,
pub(crate) component_index: &'protocol HashMap<String, u32>,
pub(crate) style_resource_index: &'protocol HashMap<String, u32>,
pub(crate) style_resources_requiring_escape: &'protocol HashSet<String>,
pub(crate) style_chunk_index: HashMap<&'protocol str, u32>,
pub(crate) css_strategy: webui_protocol::CssStrategy,
pub(crate) head_inject: Option<&'protocol str>,
pub(crate) body_inject: Option<&'protocol str>,
pub(crate) state_inject: StateInject<'state>,
pub(crate) head_end_emitted: bool,
pub(crate) body_start_emitted: bool,
pub(crate) component_asset_styles_emitted: bool,
pub(crate) body_end_emitted: bool,
pub(crate) route_index: &'protocol CompiledRouteIndex,
pub(crate) route_chain_index: usize,
pub(crate) route_chain: Option<Vec<crate::route_handler::RouteChainEntry>>,
pub(crate) route_document_style_targets: Vec<bool>,
pub(crate) reachable_components: Option<Vec<String>>,
pub(crate) streaming: Option<&'output mut StreamingRenderState<'protocol>>,
pub(crate) json_scratch: Vec<u8>,
pub(crate) scope_pool: Vec<ScopeMap>,
pub(crate) document_style_resources: HashSet<String>,
pub(crate) shadow_style_roots: Vec<ShadowStyleRoot>,
borrowed_scope_pool: Vec<BorrowedScope<'protocol, 'state>>,
}
pub(crate) const STRUCTURAL_SIGNAL_PREFIX: &str = "}}}webui:";
pub(crate) fn structural_signal_value(
signal: &webui_protocol::WebUIFragmentSignal,
) -> Option<&str> {
if !signal.raw {
return None;
}
signal.value.strip_prefix(STRUCTURAL_SIGNAL_PREFIX)
}
pub(crate) fn write_interaction_marker(
tag: &str,
context: &mut WebUIProcessContext<'_, '_, '_>,
) -> Result<()> {
if matches!(
context
.protocol
.components
.get(tag)
.and_then(|component| ComponentWorkPolicy::try_from(component.work_policy).ok()),
Some(ComponentWorkPolicy::Interaction | ComponentWorkPolicy::LazyRenderInteraction)
) {
context.writer.write(" data-webui-interaction")?;
}
Ok(())
}
fn doctype_prefix_end(raw: &str) -> Option<usize> {
const PREFIX: &[u8] = b"<!doctype";
let bytes = raw.as_bytes();
let mut cursor = usize::from(bytes.starts_with(b"\xEF\xBB\xBF")) * 3;
while bytes
.get(cursor)
.is_some_and(|byte| byte.is_ascii_whitespace())
{
cursor += 1;
}
let prefix_end = cursor.checked_add(PREFIX.len())?;
if !bytes.get(cursor..prefix_end)?.eq_ignore_ascii_case(PREFIX) {
return None;
}
let separator = *bytes.get(prefix_end)?;
if separator != b'>' && !separator.is_ascii_whitespace() {
return None;
}
let mut quote = None;
for (offset, byte) in bytes[prefix_end..].iter().copied().enumerate() {
match quote {
Some(active) if byte == active => quote = None,
Some(_) => {}
None if matches!(byte, b'\'' | b'"') => quote = Some(byte),
None if byte == b'>' => return Some(prefix_end + offset + 1),
None => {}
}
}
None
}
const SCOPE_POOL_CAP: usize = 8;
const INLINE_SCOPE_SLOTS: usize = 4;
const INLINE_RENDER_FRAGMENT_LISTS: usize = 8;
const INLINE_STYLE_UNITS: usize = 8;
const COMPONENT_ASSET_MANIFEST_ID: &str = "webui-component-assets";
struct EmittedStyleUnits<'a> {
inline: [&'a str; INLINE_STYLE_UNITS],
inline_len: usize,
overflow: HashSet<&'a str>,
}
impl<'a> EmittedStyleUnits<'a> {
fn new() -> Self {
Self {
inline: [""; INLINE_STYLE_UNITS],
inline_len: 0,
overflow: HashSet::new(),
}
}
fn insert(&mut self, name: &'a str) -> bool {
if self.inline[..self.inline_len].contains(&name) {
return false;
}
if self.inline_len < INLINE_STYLE_UNITS {
self.inline[self.inline_len] = name;
self.inline_len += 1;
return true;
}
self.overflow.insert(name)
}
}
struct ComponentAssetStyleManifest<'a>(&'a [ComponentAssetStylePreload]);
impl Serialize for ComponentAssetStyleManifest<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for entry in self.0 {
map.serialize_entry(&entry.root, &entry.style_hrefs)?;
}
map.end()
}
}
fn serialize_component_asset_style_manifest(
preloads: &[ComponentAssetStylePreload],
) -> std::result::Result<String, serde_json::Error> {
if preloads.is_empty() {
return Ok(String::new());
}
let json = serde_json::to_string(&ComponentAssetStyleManifest(preloads))?;
if json.contains("</") {
Ok(json.replace("</", "<\\/"))
} else {
Ok(json)
}
}
fn serialize_component_asset_style_links(preloads: &[ComponentAssetStylePreload]) -> String {
let href_count = preloads.iter().map(|entry| entry.style_hrefs.len()).sum();
let mut seen = HashSet::with_capacity(href_count);
let mut links = String::with_capacity(href_count.saturating_mul(48));
for entry in preloads {
for href in &entry.style_hrefs {
if !seen.insert(href.as_str()) {
continue;
}
links.push_str("<link rel=\"stylesheet\" href=\"");
push_escaped_html_attribute(&mut links, href);
links.push_str("\">");
}
}
links
}
fn push_escaped_html_attribute(output: &mut String, value: &str) {
let mut start = 0;
for (index, ch) in value.char_indices() {
let escaped = match ch {
'&' => Some("&"),
'"' => Some("""),
'<' => Some("<"),
'>' => Some(">"),
_ => None,
};
let Some(entity) = escaped else {
continue;
};
if start < index {
output.push_str(&value[start..index]);
}
output.push_str(entity);
start = index + ch.len_utf8();
}
if start < value.len() {
output.push_str(&value[start..]);
}
}
fn remove_owned_component_attr(attrs: &mut ScopeMap, name: &str) {
if !attrs.is_empty() {
attrs.remove(name);
}
}
fn take_scope_map(pool: &mut Vec<ScopeMap>) -> ScopeMap {
pool.pop().unwrap_or_default()
}
fn recycle_scope_map(pool: &mut Vec<ScopeMap>, mut map: ScopeMap) {
if pool.len() < SCOPE_POOL_CAP {
map.clear();
pool.push(map);
}
}
fn take_borrowed_scope<'protocol, 'state>(
pool: &mut Vec<BorrowedScope<'protocol, 'state>>,
) -> BorrowedScope<'protocol, 'state> {
pool.pop().unwrap_or_default()
}
fn recycle_borrowed_scope<'protocol, 'state>(
pool: &mut Vec<BorrowedScope<'protocol, 'state>>,
mut scope: BorrowedScope<'protocol, 'state>,
) {
if pool.len() < SCOPE_POOL_CAP {
scope.clear();
pool.push(scope);
}
}
pub(crate) enum WebUiBootstrapState<'a> {
Complete {
value: &'a Value,
selection: StateSelection<'a>,
},
Reference {
record_sequence: usize,
value: &'a Value,
delta: Option<StateSelection<'a>>,
},
}
pub(crate) struct WebUiBootstrap<'a, ComponentStyles: Serialize + ?Sized = Value> {
pub(crate) declaration_id: Option<u32>,
pub(crate) enclosing_span_instance_id: Option<u32>,
pub(crate) state: WebUiBootstrapState<'a>,
pub(crate) chain: &'a [Value],
pub(crate) inventory: &'a str,
pub(crate) nonce: Option<&'a str>,
pub(crate) css_hrefs: &'a [&'a str],
pub(crate) style_specs: &'a [&'a str],
pub(crate) component_styles: &'a ComponentStyles,
pub(crate) templates: &'a [WebUiTemplatePayload<'a>],
}
#[cfg(test)]
fn component_attr_name(name: &str) -> String {
let mut buffer = String::new();
push_component_attr_name(&mut buffer, name);
buffer
}
fn push_component_attr_name(buffer: &mut String, name: &str) {
let stripped = name.strip_prefix(':').unwrap_or(name);
if let Some(property) = webui_protocol::attrs::attribute_to_property(stripped) {
buffer.push_str(property);
return;
}
buffer.reserve(stripped.len());
let mut capitalize_next = false;
for ch in stripped.chars() {
if ch == '-' {
capitalize_next = true;
} else if capitalize_next {
buffer.extend(ch.to_uppercase());
capitalize_next = false;
} else {
buffer.push(ch);
}
}
}
#[cold]
#[inline(never)]
fn missing_component_attr_name_error() -> HandlerError {
HandlerError::Invariant("prepared component attribute name is missing".to_string())
}
struct ScalarBuffer {
buf: [u8; 48],
len: usize,
}
impl ScalarBuffer {
#[inline]
const fn new() -> Self {
Self {
buf: [0; 48],
len: 0,
}
}
#[inline]
fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.buf[..self.len]).ok()
}
}
impl fmt::Write for ScalarBuffer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
let bytes = s.as_bytes();
let end = self.len.checked_add(bytes.len()).ok_or(fmt::Error)?;
let slot = self.buf.get_mut(self.len..end).ok_or(fmt::Error)?;
slot.copy_from_slice(bytes);
self.len = end;
Ok(())
}
}
fn format_plain_json_scalar<'buf>(
buffer: &'buf mut ScalarBuffer,
value: &Value,
) -> Option<&'buf str> {
match value {
Value::Bool(true) => Some("true"),
Value::Bool(false) => Some("false"),
Value::Null => Some("null"),
Value::Number(number) => {
write!(buffer, "{number}").ok()?;
buffer.as_str()
}
_ => None,
}
}
fn write_plain_json_scalar(writer: &mut dyn ResponseWriter, value: &Value) -> Result<bool> {
let mut buffer = ScalarBuffer::new();
match format_plain_json_scalar(&mut buffer, value) {
Some(rendered) => {
writer.write(rendered)?;
Ok(true)
}
None => Ok(false),
}
}
pub(crate) fn write_usize(writer: &mut dyn ResponseWriter, mut n: usize) -> Result<()> {
if n == 0 {
return writer.write("0");
}
let mut buf = [0u8; 20];
let mut pos = buf.len();
while n > 0 {
pos -= 1;
#[allow(clippy::cast_possible_truncation)]
let digit = (n % 10) as u8;
buf[pos] = b'0' + digit;
n /= 10;
}
match std::str::from_utf8(&buf[pos..]) {
Ok(s) => writer.write(s),
Err(_) => writer.write("0"),
}
}
pub(crate) fn write_script_safe_json<T>(
writer: &mut dyn ResponseWriter,
scratch: &mut Vec<u8>,
value: &T,
) -> Result<()>
where
T: Serialize + ?Sized,
{
scratch.clear();
serde_json::to_writer(&mut *scratch, value)
.map_err(|error| HandlerError::Rendering(format!("failed to serialize JSON: {error}")))?;
let json = std::str::from_utf8(scratch)
.map_err(|error| HandlerError::Rendering(format!("invalid JSON UTF-8: {error}")))?;
write_script_safe_json_str(writer, json)
}
static END_TAG_OPENER: LazyLock<memchr::memmem::Finder<'static>> =
LazyLock::new(|| memchr::memmem::Finder::new(b"</"));
fn write_script_safe_json_str(writer: &mut dyn ResponseWriter, json: &str) -> Result<()> {
let bytes = json.as_bytes();
let mut start = 0;
if let Some(first) = memchr::memchr(b'<', bytes) {
for offset in END_TAG_OPENER.find_iter(&bytes[first..]) {
let index = first + offset;
if index > start {
writer.write(&json[start..index])?;
}
writer.write("<\\/")?;
start = index + 2;
}
}
if start < json.len() {
writer.write(&json[start..])?;
}
Ok(())
}
fn write_json_field_name(
writer: &mut dyn ResponseWriter,
wrote_field: &mut bool,
name: &str,
) -> Result<()> {
if *wrote_field {
writer.write(",")?;
}
*wrote_field = true;
writer.write("\"")?;
writer.write(name)?;
writer.write("\":")
}
fn write_json_field<T>(
writer: &mut dyn ResponseWriter,
scratch: &mut Vec<u8>,
wrote_field: &mut bool,
name: &str,
value: &T,
) -> Result<()>
where
T: Serialize + ?Sized,
{
write_json_field_name(writer, wrote_field, name)?;
write_script_safe_json(writer, scratch, value)
}
struct ProjectedState<'a> {
value: &'a Value,
keys: KeyView<'a>,
}
impl Serialize for ProjectedState<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let Value::Object(map) = self.value else {
return serializer.serialize_map(Some(0))?.end();
};
let mut out = serializer.serialize_map(None)?;
if self.keys.len() < map.len() {
let mut previous = None;
for key in self.keys.iter() {
if key == STATE_INJECT_KEY {
continue;
}
if previous == Some(key) {
continue;
}
previous = Some(key);
if let Some(value) = map.get(key) {
out.serialize_entry(key, value)?;
}
}
} else {
for (key, value) in map {
if key == STATE_INJECT_KEY {
continue;
}
if self.keys.contains(key.as_str()) {
out.serialize_entry(key, value)?;
}
}
}
out.end()
}
}
struct StateWithoutReservedKey<'a> {
value: &'a Value,
}
impl Serialize for StateWithoutReservedKey<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let Value::Object(map) = self.value else {
return self.value.serialize(serializer);
};
let mut out = serializer.serialize_map(Some(map.len().saturating_sub(1)))?;
for (key, value) in map {
if key == STATE_INJECT_KEY {
continue;
}
out.serialize_entry(key, value)?;
}
out.end()
}
}
fn write_full_state(
writer: &mut dyn ResponseWriter,
scratch: &mut Vec<u8>,
state: &Value,
) -> Result<()> {
if matches!(state, Value::Object(map) if map.contains_key(STATE_INJECT_KEY)) {
return write_script_safe_json(writer, scratch, &StateWithoutReservedKey { value: state });
}
write_script_safe_json(writer, scratch, state)
}
pub(crate) fn write_selected_state(
writer: &mut dyn ResponseWriter,
scratch: &mut Vec<u8>,
state: &Value,
selection: &StateSelection<'_>,
) -> Result<()> {
let keys = match selection {
StateSelection::Full => return write_full_state(writer, scratch, state),
StateSelection::Keys(keys) => KeyView::Borrowed(keys.as_slice()),
StateSelection::KeyIds(selection) => KeyView::Ids(*selection),
StateSelection::FullExceptKeyIds(selection) => {
return write_script_safe_json(
writer,
scratch,
&StateWithoutSelectedKeys {
value: state,
keys: KeyView::Ids(*selection),
},
);
}
};
if keys.is_empty() {
return writer.write("{}");
}
debug_assert!(
keys.iter()
.zip(keys.iter().skip(1))
.all(|(left, right)| left <= right),
"hydration keys must be sorted for binary-search projection"
);
if let Value::Object(map) = state {
let selects_entire_map =
keys.len() == map.len() && keys.iter().eq(map.keys().map(String::as_str));
if selects_entire_map {
return write_full_state(writer, scratch, state);
}
}
write_script_safe_json(writer, scratch, &ProjectedState { value: state, keys })
}
pub(crate) const INITIAL_KEY_CAPACITY: usize = 16;
pub(crate) enum StateSelection<'a> {
Full,
Keys(Vec<&'a str>),
KeyIds(HydrationKeySelection<'a>),
FullExceptKeyIds(HydrationKeySelection<'a>),
}
#[derive(Clone, Copy)]
pub(crate) struct HydrationKeySelection<'a> {
pub(crate) ids: &'a [u32],
pub(crate) index: &'a ComponentReachabilityIndex,
}
impl<'a> HydrationKeySelection<'a> {
fn key(self, position: usize) -> Option<&'a str> {
let id = self.ids.get(position).copied()?;
self.index.hydration_key(id)
}
}
#[derive(Clone, Copy)]
enum KeyView<'a> {
Borrowed(&'a [&'a str]),
Ids(HydrationKeySelection<'a>),
}
impl<'a> KeyView<'a> {
fn len(self) -> usize {
match self {
Self::Borrowed(keys) => keys.len(),
Self::Ids(selection) => selection.ids.len(),
}
}
fn is_empty(self) -> bool {
self.len() == 0
}
fn get(self, position: usize) -> Option<&'a str> {
match self {
Self::Borrowed(keys) => keys.get(position).copied(),
Self::Ids(selection) => selection.key(position),
}
}
fn iter(self) -> impl Iterator<Item = &'a str> {
(0..self.len()).map_while(move |position| self.get(position))
}
fn contains(self, key: &str) -> bool {
match self {
Self::Borrowed(keys) => keys
.binary_search_by(|candidate| str::cmp(candidate, key))
.is_ok(),
Self::Ids(selection) => selection
.ids
.binary_search_by(|id| {
selection
.index
.hydration_key(*id)
.map_or(std::cmp::Ordering::Less, |candidate| {
str::cmp(candidate, key)
})
})
.is_ok(),
}
}
}
#[derive(Clone, Copy)]
enum ComponentStateSurface {
Hydration,
Navigation,
}
pub(crate) fn collect_hydration_state<'a, 'b>(
protocol: &'a WebUIProtocol,
components: impl IntoIterator<Item = &'b str>,
) -> StateSelection<'a> {
if protocol.initial_state_strategy != InitialStateStrategy::Components as i32 {
return StateSelection::Full;
}
collect_component_state(protocol, components, ComponentStateSurface::Hydration)
}
pub(crate) fn collect_hydration_key_ids_into(
protocol: &WebUIProtocol,
index: &ComponentReachabilityIndex,
components: impl IntoIterator<Item = u32>,
ids: &mut Vec<u32>,
) -> bool {
ids.clear();
if protocol.initial_state_strategy != InitialStateStrategy::Components as i32 {
return true;
}
for component in components {
if index.extend_hydration_keys(component, ids) {
ids.clear();
return true;
}
}
ids.sort_unstable();
ids.dedup();
false
}
pub(crate) fn collect_navigation_state<'a, 'b>(
protocol: &'a WebUIProtocol,
components: impl IntoIterator<Item = &'b str>,
) -> StateSelection<'a> {
collect_component_state(protocol, components, ComponentStateSurface::Navigation)
}
fn collect_component_state<'a, 'b>(
protocol: &'a WebUIProtocol,
components: impl IntoIterator<Item = &'b str>,
surface: ComponentStateSurface,
) -> StateSelection<'a> {
let mut keys = Vec::with_capacity(INITIAL_KEY_CAPACITY);
if collect_component_state_into(protocol, components, surface, &mut keys) {
StateSelection::Full
} else {
StateSelection::Keys(keys)
}
}
fn collect_component_state_into<'a, 'b>(
protocol: &'a WebUIProtocol,
components: impl IntoIterator<Item = &'b str>,
surface: ComponentStateSurface,
keys: &mut Vec<&'a str>,
) -> bool {
keys.clear();
for name in components {
let Some(component) = protocol.components.get(name) else {
return true;
};
let (mode, component_keys) = match surface {
ComponentStateSurface::Hydration => {
(component.hydration_mode, &component.hydration_keys)
}
ComponentStateSurface::Navigation => (
match component.navigation_mode {
Some(mode) => mode,
None if !component.navigation_keys.is_empty() => {
StateProjectionMode::Keys as i32
}
None => return true,
},
&component.navigation_keys,
),
};
if mode == StateProjectionMode::All as i32 {
return true;
}
if mode == StateProjectionMode::Keys as i32
|| (mode == StateProjectionMode::None as i32 && !component_keys.is_empty())
{
keys.extend(component_keys.iter().map(String::as_str));
} else if mode != StateProjectionMode::None as i32 {
return true;
}
}
keys.sort_unstable();
keys.dedup();
false
}
pub(crate) fn write_webui_bootstrap<ComponentStyles: Serialize + ?Sized>(
writer: &mut dyn ResponseWriter,
scratch: &mut Vec<u8>,
bootstrap: WebUiBootstrap<'_, ComponentStyles>,
) -> Result<()> {
let mut wrote_field = false;
writer.write("{")?;
if let Some(declaration_id) = bootstrap.declaration_id {
write_json_field(
writer,
scratch,
&mut wrote_field,
"declarationId",
&declaration_id,
)?;
}
if let Some(span_id) = bootstrap.enclosing_span_instance_id {
write_json_field(
writer,
scratch,
&mut wrote_field,
"enclosingSpanInstanceId",
&span_id,
)?;
}
if !bootstrap.chain.is_empty() {
write_json_field(writer, scratch, &mut wrote_field, "chain", bootstrap.chain)?;
}
write_json_field(
writer,
scratch,
&mut wrote_field,
"componentStyles",
bootstrap.component_styles,
)?;
if !bootstrap.css_hrefs.is_empty() {
write_json_field(
writer,
scratch,
&mut wrote_field,
"css",
bootstrap.css_hrefs,
)?;
}
write_json_field(
writer,
scratch,
&mut wrote_field,
"inventory",
bootstrap.inventory,
)?;
if let Some(nonce) = bootstrap.nonce {
write_json_field(writer, scratch, &mut wrote_field, "nonce", nonce)?;
}
match bootstrap.state {
WebUiBootstrapState::Complete { value, selection } => {
write_json_field_name(writer, &mut wrote_field, "state")?;
write_selected_state(writer, scratch, value, &selection)?;
}
WebUiBootstrapState::Reference {
record_sequence,
value,
delta,
} => {
write_json_field(
writer,
scratch,
&mut wrote_field,
"stateRef",
&record_sequence,
)?;
if let Some(delta) = delta {
write_json_field_name(writer, &mut wrote_field, "stateDelta")?;
write_selected_state(writer, scratch, value, &delta)?;
}
}
}
if !bootstrap.style_specs.is_empty() {
write_json_field(
writer,
scratch,
&mut wrote_field,
"styles",
bootstrap.style_specs,
)?;
}
if bootstrap
.templates
.iter()
.any(|template| !template.template_json.is_empty())
{
write_json_field_name(writer, &mut wrote_field, "templates")?;
write_webui_template_json_map(writer, scratch, bootstrap.templates)?;
}
writer.write("}")
}
fn write_webui_data_block(
writer: &mut dyn ResponseWriter,
scratch: &mut Vec<u8>,
bootstrap: WebUiBootstrap<'_>,
) -> Result<()> {
writer.write("<script type=\"application/json\" id=\"webui-data\"")?;
if let Some(nonce) = bootstrap.nonce {
writer.write(" nonce=\"")?;
writer.write(nonce)?;
writer.write("\"")?;
}
writer.write(">")?;
write_webui_bootstrap(writer, scratch, bootstrap)?;
writer.write("</script>\n")
}
fn write_component_asset_style_manifest(
writer: &mut dyn ResponseWriter,
manifest: &str,
nonce: Option<&str>,
) -> Result<()> {
if manifest.is_empty() {
return Ok(());
}
writer.write("<script type=\"application/json\" id=\"")?;
writer.write(COMPONENT_ASSET_MANIFEST_ID)?;
writer.write("\"")?;
if let Some(nonce) = nonce {
writer.write(" nonce=\"")?;
writer.write(&crate::html_encode::encode_safe(nonce))?;
writer.write("\"")?;
}
writer.write(">")?;
writer.write(manifest)?;
writer.write("</script>")
}
fn write_component_asset_styles(context: &mut WebUIProcessContext<'_, '_, '_>) -> Result<()> {
if context.component_asset_styles_emitted {
return Ok(());
}
context.component_asset_styles_emitted = true;
context.writer.write(context.component_asset_style_links)?;
write_component_asset_style_manifest(
context.writer,
context.component_asset_style_manifest,
context.nonce,
)
}
fn write_webui_template_json_map(
writer: &mut dyn ResponseWriter,
scratch: &mut Vec<u8>,
templates: &[WebUiTemplatePayload<'_>],
) -> Result<()> {
writer.write("{")?;
let mut wrote = false;
for template in templates {
if template.template_json.is_empty() {
continue;
}
if wrote {
writer.write(",")?;
}
wrote = true;
write_script_safe_json(writer, scratch, template.tag_name)?;
writer.write(":")?;
write_script_safe_json_str(writer, template.template_json)?;
}
writer.write("}")
}
fn resolve_value_from_sources<'ctx, 'state>(
path: &str,
loop_vars: &'ctx [LoopBinding<'_, 'state>],
visible_loop_scope: VisibleLoopScope,
local_values: LocalValueSources<'ctx, '_, 'state>,
state: &'state Value,
) -> Option<Cow<'ctx, Value>>
where
'state: 'ctx,
{
if let Some(binding) = loop_vars
.get(visible_loop_scope.start..visible_loop_scope.end)
.and_then(|bindings| bindings.last())
{
let name = binding.name;
if path.len() == name.len() && path == name {
return Some(Cow::Borrowed(binding.value));
}
if path.len() > name.len()
&& path.as_bytes().get(name.len()) == Some(&b'.')
&& path.starts_with(name)
{
if let Some(value) =
find_value_by_dotted_path_ref(&path[name.len() + 1..], binding.value)
{
return Some(value);
}
return find_value_by_dotted_path_ref(path, state);
}
}
if let Some(first_part) = path.split('.').next() {
if let Some(local_value) = local_values.borrowed.get(first_part) {
if first_part.len() == path.len() {
return Some(Cow::Borrowed(local_value));
}
let remaining = &path[first_part.len() + 1..];
if let Some(value) = find_value_by_dotted_path_ref(remaining, local_value) {
return Some(value);
}
return find_value_by_dotted_path_ref(path, state);
}
if let Some(local_value) = local_values.owned.get(first_part) {
if first_part.len() == path.len() {
return Some(Cow::Borrowed(local_value));
}
let remaining = &path[first_part.len() + 1..];
if let Some(value) = find_value_by_dotted_path_ref(remaining, local_value) {
return Some(value);
}
return find_value_by_dotted_path_ref(path, state);
}
for binding in loop_vars[visible_loop_scope.start..visible_loop_scope.end]
.iter()
.rev()
{
if binding.name != first_part {
continue;
}
if first_part.len() == path.len() {
return Some(Cow::Borrowed(binding.value));
}
let remaining = &path[first_part.len() + 1..];
if let Some(value) = find_value_by_dotted_path_ref(remaining, binding.value) {
return Some(value);
}
return find_value_by_dotted_path_ref(path, state);
}
}
find_value_by_dotted_path_ref(path, state)
}
fn resolve_borrowed_collection<'state>(
path: &str,
loop_vars: &[LoopBinding<'_, 'state>],
visible_loop_scope: VisibleLoopScope,
local_values: LocalValueSources<'_, '_, 'state>,
state: &'state Value,
) -> Option<&'state [Value]> {
let first_part = path.split('.').next()?;
if let Some(binding) = loop_vars
.get(visible_loop_scope.start..visible_loop_scope.end)
.and_then(|bindings| bindings.last())
.filter(|binding| binding.name == first_part)
{
if first_part.len() == path.len() {
return binding.value.as_array().map(Vec::as_slice);
}
let remaining = &path[first_part.len() + 1..];
return match find_value_by_dotted_path_ref(remaining, binding.value) {
Some(Cow::Borrowed(value)) => value.as_array().map(Vec::as_slice),
Some(Cow::Owned(_)) => None,
None => borrowed_state_array(path, state),
};
}
if let Some(local_value) = local_values.borrowed.get(first_part) {
let local_match = if first_part.len() == path.len() {
Some(Cow::Borrowed(local_value))
} else {
find_value_by_dotted_path_ref(&path[first_part.len() + 1..], local_value)
};
return match local_match {
Some(Cow::Borrowed(value)) => value.as_array().map(Vec::as_slice),
Some(Cow::Owned(_)) => None,
None => borrowed_state_array(path, state),
};
}
if let Some(local_value) = local_values.owned.get(first_part) {
let local_match = if first_part.len() == path.len() {
Some(Cow::Borrowed(local_value))
} else {
find_value_by_dotted_path_ref(&path[first_part.len() + 1..], local_value)
};
if local_match.is_some() {
return None;
}
return borrowed_state_array(path, state);
}
for binding in loop_vars[visible_loop_scope.start..visible_loop_scope.end]
.iter()
.rev()
{
if binding.name != first_part {
continue;
}
if first_part.len() == path.len() {
return binding.value.as_array().map(Vec::as_slice);
}
let remaining = &path[first_part.len() + 1..];
return match find_value_by_dotted_path_ref(remaining, binding.value) {
Some(Cow::Borrowed(value)) => value.as_array().map(Vec::as_slice),
Some(Cow::Owned(_)) => None,
None => borrowed_state_array(path, state),
};
}
borrowed_state_array(path, state)
}
fn resolve_state_backed_value<'state>(
path: &str,
loop_vars: &[LoopBinding<'_, 'state>],
visible_loop_scope: VisibleLoopScope,
local_values: LocalValueSources<'_, '_, 'state>,
state: &'state Value,
) -> Option<&'state Value> {
let first_part = path.split('.').next()?;
if let Some(binding) = loop_vars
.get(visible_loop_scope.start..visible_loop_scope.end)
.and_then(|bindings| bindings.last())
.filter(|binding| binding.name == first_part)
{
if first_part.len() == path.len() {
return Some(binding.value);
}
let remaining = &path[first_part.len() + 1..];
return match find_value_by_dotted_path_ref(remaining, binding.value) {
Some(Cow::Borrowed(value)) => Some(value),
Some(Cow::Owned(_)) => None,
None => borrowed_state_value(path, state),
};
}
if let Some(local_value) = local_values.borrowed.get(first_part) {
if first_part.len() == path.len() {
return Some(local_value);
}
return match find_value_by_dotted_path_ref(&path[first_part.len() + 1..], local_value) {
Some(Cow::Borrowed(value)) => Some(value),
Some(Cow::Owned(_)) => None,
None => borrowed_state_value(path, state),
};
}
if let Some(local_value) = local_values.owned.get(first_part) {
if first_part.len() == path.len() {
return None;
}
return match find_value_by_dotted_path_ref(&path[first_part.len() + 1..], local_value) {
Some(_) => None,
None => borrowed_state_value(path, state),
};
}
for binding in loop_vars[visible_loop_scope.start..visible_loop_scope.end]
.iter()
.rev()
{
if binding.name != first_part {
continue;
}
if first_part.len() == path.len() {
return Some(binding.value);
}
let remaining = &path[first_part.len() + 1..];
return match find_value_by_dotted_path_ref(remaining, binding.value) {
Some(Cow::Borrowed(value)) => Some(value),
Some(Cow::Owned(_)) => None,
None => borrowed_state_value(path, state),
};
}
borrowed_state_value(path, state)
}
fn borrowed_state_value<'state>(path: &str, state: &'state Value) -> Option<&'state Value> {
match find_value_by_dotted_path_ref(path, state) {
Some(Cow::Borrowed(value)) => Some(value),
Some(Cow::Owned(_)) | None => None,
}
}
fn borrowed_state_array<'state>(path: &str, state: &'state Value) -> Option<&'state [Value]> {
match find_value_by_dotted_path_ref(path, state)? {
Cow::Borrowed(value) => value.as_array().map(Vec::as_slice),
Cow::Owned(_) => None,
}
}
impl WebUIHandler {
pub fn new() -> Self {
Self {
plugin_factory: None,
}
}
pub fn with_plugin(factory: fn() -> Box<dyn HandlerPlugin>) -> Self {
Self {
plugin_factory: Some(factory),
}
}
#[cfg(test)]
fn handle(
&self,
document: &WebUIProtocol,
state: &Value,
options: &RenderOptions<'_>,
writer: &mut dyn ResponseWriter,
) -> Result<()> {
let protocol = Protocol::new(document.clone());
self.render(&protocol, state, options, writer)
}
fn process_fragment_target<'data>(
&self,
target: Option<usize>,
fragment_id: &str,
context: &mut WebUIProcessContext<'data, '_, '_>,
) -> Result<()> {
let Some(index) = target.or_else(|| context.render_fragments.index(fragment_id)) else {
return Err(HandlerError::MissingFragment(fragment_id.to_string()));
};
let Some(fragment_list) = context.render_fragments.list(index) else {
return Err(HandlerError::MissingFragment(fragment_id.to_string()));
};
self.process_fragment(fragment_list, context)
}
fn process_fragment<'data>(
&self,
fragment_list: RenderFragmentList<'data>,
context: &mut WebUIProcessContext<'data, '_, '_>,
) -> Result<()> {
self.process_fragment_from(fragment_list, 0, context)
}
fn process_fragment_from<'data>(
&self,
fragment_list: RenderFragmentList<'data>,
start: usize,
context: &mut WebUIProcessContext<'data, '_, '_>,
) -> Result<()> {
let fragments = fragment_list.fragments;
let best_route = if fragment_list.has_routes {
route_renderer::find_best_route_match(
fragments,
context.request_path,
&context.route_base,
context.route_index,
)
} else {
None
};
self.process_fragment_range(fragment_list, start..fragments.len(), &best_route, context)
}
fn process_fragment_range<'data>(
&self,
fragment_list: RenderFragmentList<'data>,
range: std::ops::Range<usize>,
best_route: &Option<(String, route_matcher::RouteMatch)>,
context: &mut WebUIProcessContext<'data, '_, '_>,
) -> Result<()> {
let fragments = fragment_list.fragments;
let Some(selected) = fragments.get(range.clone()) else {
return Err(invalid_fragment_range_error(&range, fragments.len()));
};
for (offset, item) in selected.iter().enumerate() {
let index = range.start + offset;
if context.streaming.is_some() {
validate_pending_streaming_root(item, context)?;
validate_streaming_root_opening(&fragments[..index], item)?;
}
match item.fragment.as_ref() {
Some(Fragment::Raw(raw)) => {
context.writer.write(&raw.value)?;
}
Some(Fragment::Component(component)) => {
self.process_component(
&component.fragment_id,
fragment_list.target(index),
ComponentHostOrigin::ParserProduced,
context,
)?;
}
Some(Fragment::ForLoop(for_loop)) => {
self.process_for_loop(for_loop, fragment_list.target(index), context)?;
}
Some(Fragment::Signal(signal)) => {
self.process_signal(signal, context)?;
}
Some(Fragment::IfCond(if_cond)) => {
self.process_if(if_cond, fragment_list.target(index), context)?;
}
Some(Fragment::Attribute(attr)) => {
self.process_attribute(
attr,
fragment_list.target(index),
fragment_list.component_attr_name(index),
context,
)?;
}
Some(Fragment::Plugin(plugin_frag)) => {
if let Some(p) = &mut context.plugin {
p.on_element_data(&plugin_frag.data, context.writer)?;
}
}
Some(Fragment::Route(route_frag)) => {
self.process_route(route_frag, best_route, context)?;
}
Some(Fragment::Outlet(_)) => {
self.process_outlet(context)?;
}
Some(Fragment::Boundary(_)) => {}
None => {}
}
}
ensure_no_pending_streaming_root(context, "the end of the containing fragment")
}
fn process_outlet<'protocol>(
&self,
context: &mut WebUIProcessContext<'protocol, '_, '_>,
) -> Result<()> {
let mut children = std::mem::take(&mut context.route_children);
if children.is_empty() {
return Ok(());
}
let request_segments = route_matcher::split_request_path(context.request_path);
let mut best: Option<(usize, route_matcher::RouteMatch)> = None;
for (idx, child) in children.iter().enumerate() {
if let Some(m) = route_matcher::match_route_indexed_with_segments(
context.route_index,
&child.path,
&context.route_base,
&request_segments,
child.exact,
) {
let is_better = best
.as_ref()
.is_none_or(|(_, prev)| m.specificity > prev.specificity);
if is_better {
best = Some((idx, m));
}
}
}
let after_match = best.as_ref().map_or(0, |(idx, _)| idx + 1);
if let Some((idx, ref rm)) = best {
route_renderer::write_hidden_routes(context.writer, &children[..idx])?;
let descended = descend_into(&mut children, idx);
let Some(matched_child) = children.get(idx) else {
return Ok(());
};
let comp = &matched_child.fragment_id;
if !comp.is_empty() {
let saved_route_base = (rm.consumed_segments > 0).then(|| {
let base = route_matcher::compute_route_base(
context.request_path,
rm.consumed_segments,
);
std::mem::replace(&mut context.route_base, Cow::Owned(base))
});
context.route_children = descended;
context.writer.write("<webui-route")?;
context.writer.write(" path=\"")?;
context.writer.write(&matched_child.path)?;
context.writer.write("\"")?;
context.writer.write(" component=\"")?;
context.writer.write(comp)?;
context.writer.write("\"")?;
if matched_child.exact {
context.writer.write(" exact")?;
}
route_renderer::write_route_navigation_attrs(context.writer, matched_child)?;
let ri = context.route_chain_index;
context.route_chain_index += 1;
context.writer.write(" data-ri=\"")?;
write_usize(context.writer, ri)?;
context.writer.write("\" active>")?;
if !Self::component_owns_css_tree(comp, context.protocol) {
self.emit_component_style_closure(comp, StyleClosureInstall::Routed, context)?;
}
if !matched_child.content_fragment_id.is_empty() {
self.process_fragment_target(
None,
&matched_child.content_fragment_id,
context,
)?;
}
context.writer.write("<")?;
context.writer.write(comp)?;
if let Some(p) = &context.plugin {
p.write_route_component_state(context.state, context.writer)?;
}
write_interaction_marker(comp, context)?;
prepare_generated_streaming_root(comp, context)?;
context.writer.write(">")?;
self.process_component(comp, None, ComponentHostOrigin::HandlerGenerated, context)?;
context.writer.write("</")?;
context.writer.write(comp)?;
context.writer.write(">")?;
context.writer.write("</webui-route>")?;
if let Some(saved) = saved_route_base {
context.route_base = saved;
}
context.route_children = Cow::Borrowed(&[]);
}
}
route_renderer::write_hidden_routes(context.writer, &children[after_match..])
}
fn emit_css_module_importmap(
&self,
specifier: &str,
css: &str,
context: &mut WebUIProcessContext,
) -> Result<()> {
let tag = crate::css_module::build_importmap_tag(specifier, css, context.nonce);
context.writer.write(&tag)?;
Ok(())
}
fn emit_component_style_closure(
&self,
root: &str,
install: StyleClosureInstall,
context: &mut WebUIProcessContext,
) -> Result<()> {
if context.protocol.style_closures.is_empty() {
return Ok(());
}
let shadow_static_closure = if install == StyleClosureInstall::Routed {
match context.shadow_style_roots.last() {
Some(shadow_root) => {
if !shadow_root.static_closure_emitted {
return Err(HandlerError::Invariant(
"routed styles reached a ShadowRoot before its static style hook"
.to_string(),
));
}
let root_name = context
.component_index
.iter()
.find_map(|(name, index)| {
(*index == shadow_root.component_index).then_some(name.as_str())
})
.ok_or_else(|| {
HandlerError::Invariant(
"active Shadow style root lost its protocol index".to_string(),
)
})?;
Some(
context
.protocol
.style_closures
.get(root_name)
.ok_or_else(|| {
HandlerError::Invariant(format!(
"component style closure metadata is missing Shadow root `{root_name}`"
))
})?,
)
}
None => None,
}
} else {
None
};
let closure = context.protocol.style_closures.get(root).ok_or_else(|| {
HandlerError::Invariant(format!(
"component style closure metadata is missing root `{root}`"
))
})?;
let is_document_tree = context.shadow_style_roots.is_empty();
let strategy = context.css_strategy;
let unit_count = WebUIProtocol::style_closure_unit_count(closure);
let mut emitted_resources = EmittedStyleUnits::new();
for position in 0..unit_count {
let unit = context
.protocol
.style_closure_unit(closure, &context.style_chunk_index, position)
.ok_or_else(|| {
HandlerError::Invariant(format!(
"component style closure `{root}` references out-of-range unit {position}"
))
})?;
let (name, chunk) = (unit.name, unit.chunk);
if !emitted_resources.insert(name) {
continue;
}
let resource = unit.resource.ok_or_else(|| match chunk {
Some(index) => HandlerError::Invariant(format!(
"component style closure `{root}` references missing style chunk {index}"
)),
None => HandlerError::Invariant(format!(
"component style closure `{root}` references missing resource `{name}`"
)),
})?;
if is_document_tree && !context.document_style_resources.insert(name.to_string()) {
continue;
}
if let Some(static_closure) = shadow_static_closure {
let resource_index = match chunk {
Some(index) => {
if static_closure.style_chunks.contains(&index) {
continue;
}
index
}
None => {
if static_closure.component_tags.iter().any(|tag| tag == name) {
continue;
}
context.component_index.get(name).copied().ok_or_else(|| {
HandlerError::Invariant(format!(
"component style resource `{name}` is missing its protocol index"
))
})?
}
};
let shadow_root = context.shadow_style_roots.last_mut().ok_or_else(|| {
HandlerError::Invariant(
"active Shadow style root disappeared during routed style delivery"
.to_string(),
)
})?;
if shadow_root.routed_resources.contains(&resource_index) {
continue;
}
shadow_root.routed_resources.push(resource_index);
}
match strategy {
webui_protocol::CssStrategy::Link => {
context.writer.write("<link rel=\"stylesheet\" href=\"")?;
context
.writer
.write(&crate::html_encode::encode_safe(resource))?;
context.writer.write("\" data-webui-resource=\"")?;
context
.writer
.write(&crate::html_encode::encode_safe(name))?;
context.writer.write("\" data-webui-strategy=\"link\">")?;
}
strategy => {
context.writer.write("<style")?;
if let Some(nonce) = context.nonce {
context.writer.write(" nonce=\"")?;
context
.writer
.write(&crate::html_encode::encode_safe(nonce))?;
context.writer.write("\"")?;
}
context.writer.write(" data-webui-resource=\"")?;
context
.writer
.write(&crate::html_encode::encode_safe(name))?;
context.writer.write("\" data-webui-strategy=\"")?;
context
.writer
.write(if strategy == webui_protocol::CssStrategy::Module {
"module"
} else {
"style"
})?;
context.writer.write("\">")?;
if context.style_resources_requiring_escape.contains(name) {
crate::html_encode::write_style_text(context.writer, resource)?;
} else {
context.writer.write(resource)?;
}
context.writer.write("</style>")?;
}
}
}
Ok(())
}
pub(crate) fn ensure_request_route_chain(context: &mut WebUIProcessContext) {
if context.route_chain.is_none() {
let plan = crate::route_handler::collect_route_chain_plan(
context.protocol,
context.entry_id,
context.request_path,
context.route_index,
);
context.route_document_style_targets = plan.document_style_targets;
context.route_chain = Some(plan.entries);
}
}
fn emit_active_route_styles(
&self,
preloaded: &mut Vec<u32>,
context: &mut WebUIProcessContext,
) -> Result<()> {
Self::ensure_request_route_chain(context);
let Some(chain) = context.route_chain.take() else {
return Err(route_style_plan_missing_error());
};
let document_targets = std::mem::take(&mut context.route_document_style_targets);
if chain.len() != document_targets.len() {
let error = route_style_plan_length_error(chain.len(), document_targets.len());
context.route_document_style_targets = document_targets;
context.route_chain = Some(chain);
return Err(error);
}
let result = (|| {
for (entry, targets_document) in chain.iter().zip(&document_targets) {
if *targets_document {
self.emit_component_style_closure(
&entry.component,
StyleClosureInstall::Static,
context,
)?;
continue;
}
if context.css_strategy != webui_protocol::CssStrategy::Link {
continue;
}
self.emit_component_style_preloads(&entry.component, preloaded, context)?;
}
Ok(())
})();
context.route_document_style_targets = document_targets;
context.route_chain = Some(chain);
result
}
fn emit_component_style_preloads(
&self,
root: &str,
preloaded: &mut Vec<u32>,
context: &mut WebUIProcessContext,
) -> Result<()> {
let Some(closure) = context.protocol.style_closures.get(root) else {
return Ok(());
};
let unit_count = WebUIProtocol::style_closure_unit_count(closure);
for position in 0..unit_count {
let Some(unit) =
context
.protocol
.style_closure_unit(closure, &context.style_chunk_index, position)
else {
continue;
};
let Some(href) = unit.resource else {
continue;
};
let name = unit.name;
if context.document_style_resources.contains(name) {
continue;
}
let Some(&resource_index) = context.style_resource_index.get(name) else {
continue;
};
if preloaded.contains(&resource_index) {
continue;
}
context
.writer
.write("<link rel=\"preload\" as=\"style\" href=\"")?;
context
.writer
.write(&crate::html_encode::encode_safe(href))?;
context.writer.write("\">")?;
preloaded.push(resource_index);
}
Ok(())
}
fn emit_reachable_shadow_preloads(
&self,
preloaded: &mut Vec<u32>,
context: &mut WebUIProcessContext,
) -> Result<()> {
if context.css_strategy != webui_protocol::CssStrategy::Link {
return Ok(());
}
let reachable = context.reachable_components.take().unwrap_or_else(|| {
crate::route_handler::collect_reachable_component_order_for_request(
context.protocol,
context.entry_id,
context.request_path,
context.route_index,
)
});
let result = (|| {
for component in &reachable {
if context.protocol.component_uses_shadow_dom(component) {
self.emit_component_style_preloads(component, preloaded, context)?;
}
}
Ok(())
})();
context.reachable_components = Some(reachable);
result
}
fn process_shadow_style_signal(
&self,
root: &str,
context: &mut WebUIProcessContext,
) -> Result<()> {
let Some(root_index) = context.component_index.get(root).copied() else {
return Err(HandlerError::Invariant(format!(
"Shadow style hook `{root}` references an unknown component"
)));
};
if context
.shadow_style_roots
.last()
.map(|shadow_root| shadow_root.component_index)
!= Some(root_index)
{
return Err(HandlerError::Invariant(format!(
"Shadow style hook `{root}` does not match the active component root"
)));
}
self.emit_component_style_closure(root, StyleClosureInstall::Static, context)?;
let shadow_root = context.shadow_style_roots.last_mut().ok_or_else(|| {
HandlerError::Invariant(
"active Shadow style root disappeared after its static style hook".to_string(),
)
})?;
shadow_root.static_closure_emitted = true;
Ok(())
}
fn emit_css_module(&self, fragment_id: &str, context: &mut WebUIProcessContext) -> Result<()> {
if context.css_strategy != webui_protocol::CssStrategy::Module {
return Ok(());
}
let metadata_already_streamed = context.streaming.as_ref().is_some_and(|streaming| {
streaming_template_already_sent(streaming, context.component_index, fragment_id)
});
if !metadata_already_streamed && !context.rendered_components.contains(fragment_id) {
if let Some(css) = context
.protocol
.components
.get(fragment_id)
.map(|c| c.css.as_str())
.filter(|s| !s.is_empty())
{
self.emit_css_module_importmap(fragment_id, css, context)?;
}
}
Ok(())
}
fn process_route<'protocol>(
&self,
route_frag: &'protocol webui_protocol::WebUiFragmentRoute,
best_route: &Option<(String, route_matcher::RouteMatch)>,
context: &mut WebUIProcessContext<'protocol, '_, '_>,
) -> Result<()> {
let is_matched = best_route
.as_ref()
.is_some_and(|(best_key, _)| *best_key == route_frag.fragment_id);
context.writer.write("<webui-route")?;
context.writer.write(" path=\"")?;
context.writer.write(&route_frag.path)?;
context.writer.write("\"")?;
if !route_frag.fragment_id.is_empty() {
context.writer.write(" component=\"")?;
context.writer.write(&route_frag.fragment_id)?;
context.writer.write("\"")?;
}
if route_frag.exact {
context.writer.write(" exact")?;
}
route_renderer::write_route_navigation_attrs(context.writer, route_frag)?;
if is_matched {
let ri = context.route_chain_index;
context.route_chain_index += 1;
context.writer.write(" data-ri=\"")?;
write_usize(context.writer, ri)?;
context.writer.write("\" active>")?;
let saved_route_base = best_route.as_ref().map(|(_, rm)| {
let base =
route_matcher::compute_route_base(context.request_path, rm.consumed_segments);
std::mem::replace(&mut context.route_base, Cow::Owned(base))
});
let saved_route_children = std::mem::replace(
&mut context.route_children,
Cow::Borrowed(&route_frag.children),
);
if !route_frag.content_fragment_id.is_empty() {
self.process_fragment_target(None, &route_frag.content_fragment_id, context)?;
}
if !route_frag.fragment_id.is_empty() {
if !Self::component_owns_css_tree(&route_frag.fragment_id, context.protocol) {
self.emit_component_style_closure(
&route_frag.fragment_id,
StyleClosureInstall::Routed,
context,
)?;
}
context.writer.write("<")?;
context.writer.write(&route_frag.fragment_id)?;
if let Some(p) = &context.plugin {
p.write_route_component_state(context.state, context.writer)?;
}
write_interaction_marker(&route_frag.fragment_id, context)?;
prepare_generated_streaming_root(&route_frag.fragment_id, context)?;
context.writer.write(">")?;
self.process_component(
&route_frag.fragment_id,
None,
ComponentHostOrigin::HandlerGenerated,
context,
)?;
context.writer.write("</")?;
context.writer.write(&route_frag.fragment_id)?;
context.writer.write(">")?;
}
if let Some(saved) = saved_route_base {
context.route_base = saved;
}
context.route_children = saved_route_children;
} else {
context.writer.write(" style=\"display:none\">")?;
}
context.writer.write("</webui-route>")?;
Ok(())
}
fn process_component(
&self,
fragment_id: &str,
target: Option<usize>,
origin: ComponentHostOrigin,
context: &mut WebUIProcessContext,
) -> Result<()> {
if context.streaming.is_some() {
consume_streaming_component_root(fragment_id, origin, context)?;
record_checkpoint_tag(context, fragment_id);
}
if !context.rendered_components.contains(fragment_id) {
self.emit_css_module(fragment_id, context)?;
context.rendered_components.insert(fragment_id.to_string());
}
let owns_css_tree = Self::component_owns_css_tree(fragment_id, context.protocol);
if owns_css_tree {
Self::push_shadow_style_root(fragment_id, context)?;
}
let saved_local_vars = std::mem::take(&mut context.local_vars);
let saved_local_borrowed_vars = std::mem::take(&mut context.local_borrowed_vars);
let saved_component_attrs = std::mem::replace(
&mut context.component_attrs,
take_scope_map(&mut context.scope_pool),
);
let saved_component_borrowed_attrs = std::mem::replace(
&mut context.component_borrowed_attrs,
take_borrowed_scope(&mut context.borrowed_scope_pool),
);
context.local_vars = saved_component_attrs;
context.local_borrowed_vars = saved_component_borrowed_attrs;
context.collecting_component_attrs = false;
let saved_loop_scope = context.visible_loop_scope;
context.visible_loop_scope = VisibleLoopScope {
start: context.loop_vars.len(),
end: context.loop_vars.len(),
};
if let Some(p) = &mut context.plugin {
p.push_scope();
}
let render_result = self.process_fragment_target(target, fragment_id, context);
context.visible_loop_scope = saved_loop_scope;
if owns_css_tree {
Self::pop_shadow_style_root(fragment_id, context)?;
}
render_result?;
if let Some(p) = &mut context.plugin {
p.pop_scope();
}
let used_locals = std::mem::replace(&mut context.local_vars, saved_local_vars);
recycle_scope_map(&mut context.scope_pool, used_locals);
let used_borrowed_locals =
std::mem::replace(&mut context.local_borrowed_vars, saved_local_borrowed_vars);
recycle_borrowed_scope(&mut context.borrowed_scope_pool, used_borrowed_locals);
context.component_attrs.clear();
context.component_borrowed_attrs.clear();
Ok(())
}
#[inline]
pub(crate) fn component_owns_css_tree(component: &str, protocol: &WebUIProtocol) -> bool {
!protocol.style_closures.is_empty()
&& protocol
.components
.get(component)
.is_some_and(|data| data.uses_shadow_dom)
}
pub(crate) fn push_shadow_style_root(
component: &str,
context: &mut WebUIProcessContext,
) -> Result<()> {
if !context.protocol.style_closures.contains_key(component) {
return Err(HandlerError::Invariant(format!(
"component style closure metadata is missing Shadow root `{component}`"
)));
}
let root_index = context
.component_index
.get(component)
.copied()
.ok_or_else(|| {
HandlerError::Invariant(format!(
"Shadow component `{component}` is missing its protocol index"
))
})?;
context.shadow_style_roots.push(ShadowStyleRoot {
component_index: root_index,
static_closure_emitted: false,
routed_resources: Vec::new(),
});
Ok(())
}
pub(crate) fn pop_shadow_style_root(
component: &str,
context: &mut WebUIProcessContext,
) -> Result<()> {
context.shadow_style_roots.pop().ok_or_else(|| {
HandlerError::Invariant(format!(
"Shadow component `{component}` lost its active style root"
))
})?;
Ok(())
}
fn resolve_value_owned(
&self,
path: &str,
context: &WebUIProcessContext<'_, '_, '_>,
) -> Option<Value> {
resolve_value_from_sources(
path,
&context.loop_vars,
context.visible_loop_scope,
LocalValueSources {
owned: &context.local_vars,
borrowed: &context.local_borrowed_vars,
},
context.state,
)
.map(Cow::into_owned)
}
fn evaluate_condition(
&self,
condition: &webui_protocol::ConditionExpr,
context: &WebUIProcessContext,
) -> Result<bool> {
let loop_vars = &context.loop_vars;
let visible_loop_scope = context.visible_loop_scope;
let local_values = LocalValueSources {
owned: &context.local_vars,
borrowed: &context.local_borrowed_vars,
};
let state = context.state;
match evaluate_with_resolver(condition, |path| {
resolve_value_from_sources(path, loop_vars, visible_loop_scope, local_values, state)
}) {
Ok(result) => Ok(result),
Err(ExpressionError::MissingValue(_)) => Ok(false),
Err(e) => Err(HandlerError::Evaluation(e.to_string())),
}
}
fn process_for_loop<'protocol, 'state>(
&self,
for_loop: &'protocol webui_protocol::WebUIFragmentFor,
target: Option<usize>,
context: &mut WebUIProcessContext<'protocol, 'state, '_>,
) -> Result<()> {
if let Some(items) = resolve_borrowed_collection(
&for_loop.collection,
&context.loop_vars,
context.visible_loop_scope,
LocalValueSources {
owned: &context.local_vars,
borrowed: &context.local_borrowed_vars,
},
context.state,
) {
return self.process_borrowed_for_loop(for_loop, target, items, context);
}
self.process_owned_for_loop(for_loop, target, context)
}
fn process_borrowed_for_loop<'protocol, 'state>(
&self,
for_loop: &'protocol webui_protocol::WebUIFragmentFor,
target: Option<usize>,
items: &'state [Value],
context: &mut WebUIProcessContext<'protocol, 'state, '_>,
) -> Result<()> {
if let Some(plugin) = &mut context.plugin {
plugin.on_for_start(&for_loop.fragment_id, context.writer)?;
}
let item_name = for_loop.item.as_str();
let saved_value = context.local_vars.remove(item_name);
let saved_borrowed_value = context.local_borrowed_vars.remove(item_name);
let saved_scope = context.visible_loop_scope;
for (index, item) in items.iter().enumerate() {
if let Some(plugin) = &mut context.plugin {
plugin.on_repeat_item_start(index, context.writer)?;
plugin.push_scope();
}
context.loop_vars.push(LoopBinding {
name: item_name,
value: item,
});
context.visible_loop_scope.end = context.loop_vars.len();
self.process_fragment_target(target, &for_loop.fragment_id, context)?;
context.loop_vars.pop();
context.visible_loop_scope = saved_scope;
if let Some(plugin) = &mut context.plugin {
plugin.pop_scope();
plugin.on_repeat_item_end(index, context.writer)?;
}
}
if let Some(value) = saved_value {
context.local_vars.insert(item_name.to_string(), value);
}
if let Some(value) = saved_borrowed_value {
context.local_borrowed_vars.insert(item_name, value);
}
if let Some(plugin) = &mut context.plugin {
plugin.on_for_end(&for_loop.fragment_id, context.writer)?;
}
Ok(())
}
fn process_owned_for_loop<'protocol, 'state>(
&self,
for_loop: &'protocol webui_protocol::WebUIFragmentFor,
target: Option<usize>,
context: &mut WebUIProcessContext<'protocol, 'state, '_>,
) -> Result<()> {
let collection_name = &for_loop.collection;
let items = match self.resolve_value_owned(collection_name, context) {
Some(Value::Array(arr)) => arr,
Some(_) => {
return Err(HandlerError::TypeError(format!(
"Collection '{}' is not an array",
collection_name
)))
}
None => Vec::new(),
};
if let Some(p) = &mut context.plugin {
p.on_for_start(&for_loop.fragment_id, context.writer)?;
}
let item_name = for_loop.item.as_str();
let saved_value = context.local_vars.remove(item_name);
let saved_borrowed_value = context.local_borrowed_vars.remove(item_name);
if !items.is_empty() {
context
.local_vars
.insert(item_name.to_string(), Value::Null);
}
for (i, item) in items.into_iter().enumerate() {
if let Some(p) = &mut context.plugin {
p.on_repeat_item_start(i, context.writer)?;
p.push_scope();
}
if let Some(slot) = context.local_vars.get_mut(item_name) {
*slot = item;
}
self.process_fragment_target(target, &for_loop.fragment_id, context)?;
if let Some(p) = &mut context.plugin {
p.pop_scope();
p.on_repeat_item_end(i, context.writer)?;
}
}
match saved_value {
Some(value) => {
context.local_vars.insert(item_name.to_string(), value);
}
None => {
context.local_vars.remove(item_name);
}
}
if let Some(value) = saved_borrowed_value {
context.local_borrowed_vars.insert(item_name, value);
}
if let Some(p) = &mut context.plugin {
p.on_for_end(&for_loop.fragment_id, context.writer)?;
}
Ok(())
}
#[inline]
fn process_signal<'data>(
&self,
signal: &'data webui_protocol::WebUIFragmentSignal,
context: &mut WebUIProcessContext<'data, '_, '_>,
) -> Result<()> {
if signal.raw {
self.process_raw_signal(signal, context)
} else {
self.process_state_signal(signal, false, context)
}
}
#[inline(never)]
fn process_raw_signal<'data>(
&self,
signal: &'data webui_protocol::WebUIFragmentSignal,
context: &mut WebUIProcessContext<'data, '_, '_>,
) -> Result<()> {
let Some(structural_value) = structural_signal_value(signal) else {
return self.process_state_signal(signal, !signal.raw_text_context, context);
};
if let Some(root) = structural_value.strip_prefix("shadow_styles:") {
return self.process_shadow_style_signal(root, context);
}
if context.streaming.is_some()
&& self.process_streaming_signal(structural_value, context)?
{
return Ok(());
}
if structural_value == "head_end" && !context.head_end_emitted {
context.head_end_emitted = true;
if let Some(nonce) = context.nonce {
context
.writer
.write("<meta name=\"webui-nonce\" content=\"")?;
context
.writer
.write(&crate::html_encode::encode_safe(nonce))?;
context.writer.write("\">")?;
}
if !context.protocol.component_render_css.is_empty() {
context.writer.write("<style data-webui-render-policy")?;
if let Some(nonce) = context.nonce {
context.writer.write(" nonce=\"")?;
context
.writer
.write(&crate::html_encode::encode_safe(nonce))?;
context.writer.write("\"")?;
}
context.writer.write(">")?;
context
.writer
.write(&context.protocol.component_render_css)?;
context.writer.write("</style>")?;
}
if !context.protocol.style_closures.is_empty() {
let entry_id = context.entry_id;
self.emit_component_style_closure(entry_id, StyleClosureInstall::Static, context)?;
let mut preloaded = Vec::new();
self.emit_active_route_styles(&mut preloaded, context)?;
self.emit_reachable_shadow_preloads(&mut preloaded, context)?;
}
if !context.protocol.module_preloads.is_empty() {
for href in &context.protocol.module_preloads {
context
.writer
.write("<link rel=\"modulepreload\" href=\"")?;
context.writer.write(href)?;
context.writer.write("\">")?;
}
}
write_component_asset_styles(context)?;
if let Some(html) = context.head_inject {
context.writer.write(html)?;
}
if let Some(html) = context.state_inject.head_end {
context.writer.write(html)?;
}
}
if structural_value == "body_start" && !context.body_start_emitted {
context.body_start_emitted = true;
write_component_asset_styles(context)?;
if let Some(html) = context.state_inject.body_start {
context.writer.write(html)?;
}
}
if structural_value == "body_end" && !context.body_end_emitted {
context.body_end_emitted = true;
if context.plugin.is_some() {
let reachable = context.reachable_components.take().unwrap_or_else(|| {
crate::route_handler::collect_reachable_component_order_for_request(
context.protocol,
context.entry_id,
context.request_path,
context.route_index,
)
});
let state_selection =
collect_hydration_state(context.protocol, reachable.iter().map(String::as_str));
if context.css_strategy == webui_protocol::CssStrategy::Module {
for name in &reachable {
if !context.rendered_components.contains(name) {
if let Some(css) = context
.protocol
.components
.get(name)
.map(|c| c.css.as_str())
.filter(|s| !s.is_empty())
{
self.emit_css_module_importmap(name, css, context)?;
}
}
}
}
let template_payloads = context
.plugin
.as_ref()
.and_then(|p| p.collect_template_payloads(context.protocol, &reachable));
if template_payloads.is_none() {
if let Some(ref p) = context.plugin {
p.emit_templates(
context.protocol,
&reachable,
context.nonce,
context.writer,
)?;
}
}
let inventory_hex = crate::route_handler::encode_component_inventory(
&context.rendered_components,
context.component_index,
);
Self::ensure_request_route_chain(context);
let chain = context.route_chain.as_deref().ok_or_else(|| {
HandlerError::Invariant(
"request route chain disappeared after collection".to_string(),
)
})?;
let chain_json: Vec<Value> = chain
.iter()
.map(crate::route_handler::RouteChainEntry::to_json)
.collect();
let is_link = context.css_strategy == webui_protocol::CssStrategy::Link;
let mut css_hrefs: Vec<&str> = Vec::new();
if is_link {
for name in &reachable {
if let Some(href) = context
.protocol
.components
.get(name)
.map(|c| c.css_href.as_str())
.filter(|h| !h.is_empty())
{
css_hrefs.push(href);
}
}
}
let mut style_specs: Vec<&str> = Vec::new();
if context.css_strategy == webui_protocol::CssStrategy::Module {
for name in &reachable {
if context
.protocol
.components
.get(name)
.map(|c| !c.css.is_empty())
.unwrap_or(false)
{
style_specs.push(name);
}
}
}
let empty_payloads: [WebUiTemplatePayload<'_>; 0] = [];
let payloads = template_payloads.as_deref().unwrap_or(&empty_payloads);
let mut style_roots = Vec::with_capacity(reachable.len() + chain.len() * 3 + 1);
style_roots.push(context.entry_id);
for entry in chain {
style_roots.push(entry.component.as_str());
if !entry.pending_component.is_empty() {
style_roots.push(entry.pending_component.as_str());
}
if !entry.error_component.is_empty() {
style_roots.push(entry.error_component.as_str());
}
}
style_roots.extend(reachable.iter().map(String::as_str));
let component_styles =
crate::route_handler::collect_component_styles(context.protocol, style_roots)?;
write_webui_data_block(
context.writer,
&mut context.json_scratch,
WebUiBootstrap {
declaration_id: None,
enclosing_span_instance_id: None,
state: WebUiBootstrapState::Complete {
value: context.state,
selection: state_selection,
},
chain: &chain_json,
inventory: &inventory_hex,
nonce: context.nonce,
css_hrefs: &css_hrefs,
style_specs: &style_specs,
component_styles: &component_styles,
templates: payloads,
},
)?;
if let Some(ref plugin) = context.plugin {
plugin.emit_bootstrap_extension(
BootstrapExtensionContext {
protocol: context.protocol,
components: &reachable,
payloads,
nonce: context.nonce,
},
context.writer,
)?;
}
}
if let Some(html) = context.body_inject {
context.writer.write(html)?;
}
if let Some(html) = context.state_inject.body_end {
context.writer.write(html)?;
}
}
Ok(())
}
#[inline]
fn process_state_signal(
&self,
signal: &webui_protocol::WebUIFragmentSignal,
owns_html_range: bool,
context: &mut WebUIProcessContext,
) -> Result<()> {
if let Some(p) = &mut context.plugin {
p.on_binding_start(&signal.value, owns_html_range, context.writer)?;
}
if let Some(value) = resolve_value_from_sources(
&signal.value,
&context.loop_vars,
context.visible_loop_scope,
LocalValueSources {
owned: &context.local_vars,
borrowed: &context.local_borrowed_vars,
},
context.state,
) {
self.write_signal_value(value.as_ref(), signal.raw, context.writer)?;
}
if let Some(p) = &mut context.plugin {
p.on_binding_end(&signal.value, owns_html_range, context.writer)?;
}
Ok(())
}
fn write_signal_value(
&self,
value: &Value,
raw: bool,
writer: &mut dyn ResponseWriter,
) -> Result<()> {
if write_plain_json_scalar(writer, value)? {
return Ok(());
}
if raw {
match value {
Value::String(s) => writer.write(s),
_ => writer.write(&value.to_string()),
}
} else {
match value {
Value::String(s) => writer.write(&crate::html_encode::encode_safe(s)),
_ => {
let s = value.to_string();
writer.write(&crate::html_encode::encode_safe(&s))
}
}
}
}
fn process_if(
&self,
if_cond: &webui_protocol::WebUIFragmentIf,
target: Option<usize>,
context: &mut WebUIProcessContext,
) -> Result<()> {
let condition = if_cond
.condition
.as_ref()
.ok_or_else(|| HandlerError::Rendering("If fragment missing condition".to_string()))?;
let condition_met = self.evaluate_condition(condition, context)?;
if let Some(p) = &mut context.plugin {
p.on_if_start(&if_cond.fragment_id, context.writer)?;
}
if condition_met {
if let Some(p) = &mut context.plugin {
p.push_scope();
}
self.process_fragment_target(target, &if_cond.fragment_id, context)?;
if let Some(p) = &mut context.plugin {
p.pop_scope();
}
}
if let Some(p) = &mut context.plugin {
p.on_if_end(&if_cond.fragment_id, context.writer)?;
}
Ok(())
}
fn process_attribute<'protocol, 'state>(
&self,
attr: &'protocol webui_protocol::WebUIFragmentAttribute,
template_target: Option<usize>,
component_name: Option<&'protocol str>,
context: &mut WebUIProcessContext<'protocol, 'state, '_>,
) -> Result<()> {
if attr.attr_start {
context.component_attrs.clear();
context.component_borrowed_attrs.clear();
context.collecting_component_attrs = true;
}
if let Some(condition) = &attr.condition_tree {
let condition_met = self.evaluate_condition(condition, context)?;
if context.collecting_component_attrs && !attr.attr_skip {
let name = component_name.ok_or_else(missing_component_attr_name_error)?;
context.component_borrowed_attrs.remove(name);
context
.component_attrs
.insert(name.to_owned(), Value::Bool(condition_met));
}
if condition_met {
context.writer.write(" ")?;
context.writer.write(&attr.name)?;
}
return Ok(());
}
if !attr.template.is_empty() {
let raw_value =
self.render_template_attr_value(&attr.template, template_target, context)?;
let escaped = crate::html_encode::encode_safe(&raw_value);
write_attr(context.writer, &attr.name, &escaped)?;
if context.collecting_component_attrs && !attr.attr_skip {
let name = component_name.ok_or_else(missing_component_attr_name_error)?;
context.component_borrowed_attrs.remove(name);
context
.component_attrs
.insert(name.to_owned(), Value::String(raw_value));
}
return Ok(());
}
if attr.raw_value || !attr.value.is_empty() {
if attr.raw_value {
write_attr(
context.writer,
&attr.name,
&crate::html_encode::encode_safe(&attr.value),
)?;
if context.collecting_component_attrs && !attr.attr_skip {
let name = component_name.ok_or_else(missing_component_attr_name_error)?;
context.component_borrowed_attrs.remove(name);
context
.component_attrs
.insert(name.to_owned(), Value::String(attr.value.clone()));
}
} else if attr.complex {
if context.collecting_component_attrs && !attr.attr_skip {
let state_backed_value = resolve_state_backed_value(
&attr.value,
&context.loop_vars,
context.visible_loop_scope,
LocalValueSources {
owned: &context.local_vars,
borrowed: &context.local_borrowed_vars,
},
context.state,
);
if let Some(value) = state_backed_value {
let name = component_name.ok_or_else(missing_component_attr_name_error)?;
remove_owned_component_attr(&mut context.component_attrs, name);
context.component_borrowed_attrs.insert(name, value);
} else if let Some(value) = self.resolve_value_owned(&attr.value, context) {
let name = component_name.ok_or_else(missing_component_attr_name_error)?;
context.component_borrowed_attrs.remove(name);
context.component_attrs.insert(name.to_owned(), value);
}
}
} else {
let state_backed_value = if context.collecting_component_attrs && !attr.attr_skip {
resolve_state_backed_value(
&attr.value,
&context.loop_vars,
context.visible_loop_scope,
LocalValueSources {
owned: &context.local_vars,
borrowed: &context.local_borrowed_vars,
},
context.state,
)
} else {
None
};
let value = match state_backed_value {
Some(value) => Some(Cow::Borrowed(value)),
None => resolve_value_from_sources(
&attr.value,
&context.loop_vars,
context.visible_loop_scope,
LocalValueSources {
owned: &context.local_vars,
borrowed: &context.local_borrowed_vars,
},
context.state,
),
};
match value.as_deref() {
Some(Value::String(s)) => {
write_attr(
context.writer,
&attr.name,
&crate::html_encode::encode_safe(s),
)?;
}
Some(Value::Null) | None => {
write_attr(context.writer, &attr.name, "")?;
}
Some(other) => {
let mut scalar = ScalarBuffer::new();
match format_plain_json_scalar(&mut scalar, other) {
Some(rendered) => {
write_attr(context.writer, &attr.name, rendered)?;
}
None => {
let s = other.to_string();
write_attr(
context.writer,
&attr.name,
&crate::html_encode::encode_safe(&s),
)?;
}
}
}
}
if context.collecting_component_attrs && !attr.attr_skip {
if let Some(borrowed) = state_backed_value {
let name = component_name.ok_or_else(missing_component_attr_name_error)?;
remove_owned_component_attr(&mut context.component_attrs, name);
context.component_borrowed_attrs.insert(name, borrowed);
} else {
let name = component_name.ok_or_else(missing_component_attr_name_error)?;
context.component_borrowed_attrs.remove(name);
context.component_attrs.insert(
name.to_owned(),
value
.map(Cow::into_owned)
.unwrap_or(Value::String(String::new())),
);
}
}
}
}
Ok(())
}
fn render_template_attr_value(
&self,
template_id: &str,
target: Option<usize>,
context: &WebUIProcessContext,
) -> Result<String> {
let Some(index) = target.or_else(|| context.render_fragments.index(template_id)) else {
return Err(HandlerError::MissingFragment(template_id.to_string()));
};
let fragments = context
.render_fragments
.list(index)
.ok_or_else(|| HandlerError::MissingFragment(template_id.to_string()))?;
let mut raw_value = String::with_capacity(template_attr_capacity(fragments.fragments));
for frag in fragments.fragments {
match frag.fragment.as_ref() {
Some(Fragment::Raw(raw)) => raw_value.push_str(&raw.value),
Some(Fragment::Signal(signal)) => {
if let Some(value) = resolve_value_from_sources(
&signal.value,
&context.loop_vars,
context.visible_loop_scope,
LocalValueSources {
owned: &context.local_vars,
borrowed: &context.local_borrowed_vars,
},
context.state,
) {
match value.as_ref() {
Value::String(s) => raw_value.push_str(s),
other => {
let mut scalar = ScalarBuffer::new();
match format_plain_json_scalar(&mut scalar, other) {
Some(rendered) => raw_value.push_str(rendered),
None => raw_value.push_str(&other.to_string()),
}
}
}
}
}
_ => {}
}
}
Ok(raw_value)
}
pub fn render<'a>(
&self,
protocol: &'a Protocol,
state: &'a Value,
options: &RenderOptions<'a>,
writer: &'a mut dyn ResponseWriter,
) -> Result<()> {
protocol.ensure_style_metadata()?;
let document = protocol.protocol();
let render_fragments = protocol.render_fragments().resolve(document);
let Some(entry_index) = render_fragments.index(options.entry_id) else {
return Err(HandlerError::MissingFragment(options.entry_id.to_string()));
};
let entry = render_fragments
.list(entry_index)
.ok_or_else(|| HandlerError::MissingFragment(options.entry_id.to_string()))?;
let entry_owns_css_tree = Self::component_owns_css_tree(options.entry_id, document);
let has_document_head_boundary = !entry_owns_css_tree
&& !document.style_closures.is_empty()
&& entry.fragments.iter().any(|fragment| {
matches!(
fragment.fragment.as_ref(),
Some(Fragment::Signal(signal))
if structural_signal_value(signal) == Some("head_end")
)
});
let doctype_split = (!entry_owns_css_tree
&& !has_document_head_boundary
&& !document.style_closures.is_empty())
.then(|| {
let Some(Fragment::Raw(raw)) = entry
.fragments
.first()
.and_then(|fragment| fragment.fragment.as_ref())
else {
return None;
};
doctype_prefix_end(&raw.value).map(|end| (raw.value.as_str(), end))
})
.flatten();
let component_asset_style_manifest = protocol.component_asset_style_manifest()?;
let mut context = WebUIProcessContext {
protocol: document,
render_fragments,
component_asset_style_manifest,
component_asset_style_links: protocol.component_asset_style_links(),
state,
writer,
local_vars: ScopeMap::default(),
local_borrowed_vars: BorrowedScope::default(),
loop_vars: Vec::new(),
visible_loop_scope: VisibleLoopScope::EMPTY,
component_attrs: ScopeMap::default(),
component_borrowed_attrs: BorrowedScope::default(),
collecting_component_attrs: false,
request_path: options.request_path,
route_base: Cow::Borrowed("/"),
rendered_components: HashSet::new(),
plugin: self.plugin_factory.map(|f| f()),
route_children: Cow::Borrowed(&[]),
entry_id: options.entry_id,
nonce: options.nonce.filter(|s| !s.is_empty()),
head_inject: options.head_inject.filter(|s| !s.is_empty()),
body_inject: options.body_inject.filter(|s| !s.is_empty()),
state_inject: StateInject::resolve(state),
head_end_emitted: false,
body_start_emitted: false,
component_asset_styles_emitted: false,
component_index: protocol.component_index(),
style_resource_index: protocol.style_resource_index(),
style_resources_requiring_escape: protocol.style_resources_requiring_escape(),
style_chunk_index: protocol.protocol().style_chunk_index(),
css_strategy: protocol.css_strategy(),
body_end_emitted: false,
route_index: protocol.route_index(),
route_chain_index: 0,
route_chain: None,
route_document_style_targets: Vec::new(),
reachable_components: None,
streaming: None,
json_scratch: Vec::new(),
scope_pool: Vec::new(),
document_style_resources: HashSet::new(),
shadow_style_roots: Vec::new(),
borrowed_scope_pool: Vec::new(),
};
if entry_owns_css_tree {
Self::push_shadow_style_root(options.entry_id, &mut context)?;
}
let render_result = if let Some((first_raw, split)) = doctype_split {
context.writer.write(&first_raw[..split])?;
self.emit_component_style_closure(
options.entry_id,
StyleClosureInstall::Static,
&mut context,
)?;
self.emit_active_route_styles(&mut Vec::new(), &mut context)?;
context.writer.write(&first_raw[split..])?;
self.process_fragment_from(entry, 1, &mut context)
} else {
if !entry_owns_css_tree
&& !has_document_head_boundary
&& !document.style_closures.is_empty()
{
self.emit_component_style_closure(
options.entry_id,
StyleClosureInstall::Static,
&mut context,
)?;
self.emit_active_route_styles(&mut Vec::new(), &mut context)?;
}
self.process_fragment(entry, &mut context)
};
if entry_owns_css_tree {
Self::pop_shadow_style_root(options.entry_id, &mut context)?;
}
render_result?;
writer.end()?;
Ok(())
}
}
impl Default for WebUIHandler {
fn default() -> Self {
Self::new()
}
}
const TEMPLATE_SIGNAL_CAPACITY_HINT: usize = 32;
fn template_attr_capacity(fragments: &[WebUIFragment]) -> usize {
fragments
.iter()
.map(|frag| match frag.fragment.as_ref() {
Some(Fragment::Raw(raw)) => raw.value.len(),
Some(Fragment::Signal(_)) => TEMPLATE_SIGNAL_CAPACITY_HINT,
_ => 0,
})
.sum()
}
fn write_attr(writer: &mut dyn ResponseWriter, name: &str, value: &str) -> Result<()> {
writer.write_attribute(name, value)
}
#[cfg(test)]
fn handle(
protocol: &WebUIProtocol,
state: &Value,
options: &RenderOptions<'_>,
writer: &mut dyn ResponseWriter,
) -> Result<()> {
let handler = WebUIHandler::new();
handler.handle(protocol, state, options, writer)
}
#[cfg(test)]
mod scalar_buffer_tests {
use super::*;
#[test]
fn plain_scalars_match_value_to_string() {
let cases = [
Value::Null,
Value::Bool(true),
Value::Bool(false),
serde_json::json!(0),
serde_json::json!(-1),
serde_json::json!(42),
serde_json::json!(i64::MIN),
serde_json::json!(i64::MAX),
serde_json::json!(u64::MAX),
serde_json::json!(0.5),
serde_json::json!(-0.0_f64),
serde_json::json!(1.7976931348623157e308_f64),
serde_json::json!(-1.7976931348623157e308_f64),
serde_json::json!(f64::MIN_POSITIVE),
];
for value in cases {
let mut buffer = ScalarBuffer::new();
let rendered = format_plain_json_scalar(&mut buffer, &value);
assert_eq!(
rendered,
Some(value.to_string().as_str()),
"scalar rendering diverged for {value:?}"
);
}
}
#[test]
fn non_plain_values_are_declined() {
let cases = [
serde_json::json!("<script>"),
serde_json::json!([1, 2]),
serde_json::json!({ "a": 1 }),
];
for value in cases {
let mut buffer = ScalarBuffer::new();
assert_eq!(
format_plain_json_scalar(&mut buffer, &value),
None,
"expected fallback for {value:?}"
);
}
}
#[test]
fn overflow_is_reported_instead_of_truncating() {
let mut buffer = ScalarBuffer::new();
assert!(write!(&mut buffer, "{}", "x".repeat(49)).is_err());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::streaming::STREAMING_MARKER;
use std::cell::RefCell;
use webui_parser::{ComponentRegistration, DomStrategy, HtmlParser};
use webui_protocol::{
web_ui_fragment, ComparisonOperator, ConditionExpr, FragmentList, LogicalOperator,
WebUIFragmentAttribute, WebUiFragmentRoute,
};
use webui_test_utils::test_json;
fn structural_fragment(value: impl AsRef<str>) -> WebUIFragment {
WebUIFragment::signal(
format!("{STRUCTURAL_SIGNAL_PREFIX}{}", value.as_ref()),
true,
)
}
#[test]
fn route_descent_borrows_protocol_children() {
let routes = vec![WebUiFragmentRoute {
children: vec![WebUiFragmentRoute {
fragment_id: "child".to_string(),
..Default::default()
}],
..Default::default()
}];
let mut level = RouteChildren::Borrowed(&routes);
let descended = descend_into(&mut level, 0);
let Cow::Borrowed(children) = descended else {
panic!("protocol-owned route children should remain borrowed");
};
assert!(std::ptr::eq(children, routes[0].children.as_slice()));
}
#[test]
fn route_descent_moves_parked_children() {
let mut level = RouteChildren::Owned(vec![WebUiFragmentRoute {
children: vec![WebUiFragmentRoute {
fragment_id: "child".to_string(),
..Default::default()
}],
..Default::default()
}]);
let descended = descend_into(&mut level, 0);
let Cow::Owned(children) = descended else {
panic!("parked route children must not borrow session-owned storage");
};
assert_eq!(children[0].fragment_id, "child");
let Cow::Owned(source) = level else {
panic!("the parked source level must stay owned");
};
assert!(
source[0].children.is_empty(),
"owned route children must move out instead of being cloned"
);
}
#[test]
fn borrowed_collection_reuses_state_array() {
let state = test_json!({
"items": [
{"name": "first"},
{"name": "second"}
]
});
let local_vars = ScopeMap::default();
let local_borrowed_vars = BorrowedScope::default();
let loop_vars = Vec::new();
let items = resolve_borrowed_collection(
"items",
&loop_vars,
VisibleLoopScope::EMPTY,
LocalValueSources {
owned: &local_vars,
borrowed: &local_borrowed_vars,
},
&state,
)
.unwrap_or_else(|| panic!("state array should be borrowed"));
let state_items = state["items"]
.as_array()
.unwrap_or_else(|| panic!("fixture items should be an array"));
assert!(std::ptr::eq(items.as_ptr(), state_items.as_ptr()));
}
#[test]
fn owned_local_collection_keeps_precedence() {
let state = test_json!({"items": [{"name": "global"}]});
let local_vars =
ScopeMap::from_iter([("items".to_string(), test_json!([{"name": "local"}]))]);
let local_borrowed_vars = BorrowedScope::default();
assert!(resolve_borrowed_collection(
"items",
&[],
VisibleLoopScope::EMPTY,
LocalValueSources {
owned: &local_vars,
borrowed: &local_borrowed_vars,
},
&state,
)
.is_none());
}
#[test]
fn borrowed_component_collection_reuses_state_array() {
let state = test_json!({"teams": [{"contacts": [{"name": "Avery"}]}]});
let contacts = &state["teams"][0]["contacts"];
let mut local_borrowed_vars = BorrowedScope::default();
local_borrowed_vars.insert("contacts", contacts);
let local_vars = ScopeMap::default();
let items = resolve_borrowed_collection(
"contacts",
&[],
VisibleLoopScope::EMPTY,
LocalValueSources {
owned: &local_vars,
borrowed: &local_borrowed_vars,
},
&state,
)
.unwrap_or_else(|| panic!("component collection should stay borrowed"));
let state_items = contacts
.as_array()
.unwrap_or_else(|| panic!("fixture contacts should be an array"));
assert!(std::ptr::eq(items.as_ptr(), state_items.as_ptr()));
}
#[test]
fn nested_borrowed_collection_reuses_child_array() {
let state = test_json!({
"items": [{
"children": [
{"name": "first"},
{"name": "second"}
]
}]
});
let item = &state["items"][0];
let loop_vars = vec![LoopBinding {
name: "item",
value: item,
}];
let local_vars = ScopeMap::default();
let local_borrowed_vars = BorrowedScope::default();
let children = resolve_borrowed_collection(
"item.children",
&loop_vars,
VisibleLoopScope { start: 0, end: 1 },
LocalValueSources {
owned: &local_vars,
borrowed: &local_borrowed_vars,
},
&state,
)
.unwrap_or_else(|| panic!("nested state array should be borrowed"));
let state_children = item["children"]
.as_array()
.unwrap_or_else(|| panic!("fixture children should be an array"));
assert!(std::ptr::eq(children.as_ptr(), state_children.as_ptr()));
}
#[test]
fn borrowed_resolver_preserves_loop_precedence_and_component_isolation() {
let state = test_json!({
"item": {"name": "global", "fallback": "global fallback"},
"outer": {"name": "outer"},
"inner": {"name": "inner"}
});
let loop_vars = vec![
LoopBinding {
name: "item",
value: &state["outer"],
},
LoopBinding {
name: "item",
value: &state["inner"],
},
];
let local_vars = ScopeMap::default();
let local_borrowed_vars = BorrowedScope::default();
let sources = LocalValueSources {
owned: &local_vars,
borrowed: &local_borrowed_vars,
};
assert_eq!(
resolve_value_from_sources(
"item.name",
&loop_vars,
VisibleLoopScope { start: 0, end: 2 },
sources,
&state,
)
.as_deref()
.and_then(Value::as_str),
Some("inner")
);
assert_eq!(
resolve_value_from_sources(
"item.fallback",
&loop_vars,
VisibleLoopScope { start: 0, end: 2 },
sources,
&state,
)
.as_deref()
.and_then(Value::as_str),
Some("global fallback")
);
assert_eq!(
resolve_value_from_sources(
"item.name",
&loop_vars,
VisibleLoopScope { start: 2, end: 2 },
sources,
&state,
)
.as_deref()
.and_then(Value::as_str),
Some("global")
);
}
#[test]
fn state_backed_resolver_matches_general_borrowed_resolution() {
let state = test_json!({
"global": {"name": "global"},
"inner": {"fallback": "global fallback"},
"borrowed": {"name": "borrowed"},
"items": [1, 2, 3]
});
let loop_values = test_json!({
"outer": {"name": "outer"},
"inner": {"name": "inner"}
});
let loop_vars = vec![
LoopBinding {
name: "outer",
value: &loop_values["outer"],
},
LoopBinding {
name: "inner",
value: &loop_values["inner"],
},
];
let mut local_borrowed_vars = BorrowedScope::default();
local_borrowed_vars.insert("borrowed", &state["borrowed"]);
let local_vars =
ScopeMap::from_iter([("owned".to_string(), test_json!({"name": "owned"}))]);
let sources = LocalValueSources {
owned: &local_vars,
borrowed: &local_borrowed_vars,
};
let scope = VisibleLoopScope { start: 0, end: 2 };
for path in [
"global.name",
"inner.name",
"inner.fallback",
"outer.name",
"borrowed.name",
] {
let state_backed = resolve_state_backed_value(path, &loop_vars, scope, sources, &state)
.unwrap_or_else(|| panic!("{path} should resolve from immutable state"));
let resolved = resolve_value_from_sources(path, &loop_vars, scope, sources, &state)
.unwrap_or_else(|| panic!("{path} should resolve generally"));
let Cow::Borrowed(resolved) = resolved else {
panic!("{path} should remain borrowed");
};
assert!(
std::ptr::eq(state_backed, resolved),
"{path} resolved from different sources"
);
}
assert!(
resolve_state_backed_value("owned.name", &loop_vars, scope, sources, &state).is_none(),
"owned local values must use the general resolver"
);
assert!(
resolve_state_backed_value("items.length", &loop_vars, scope, sources, &state)
.is_none(),
"synthetic values must use the general resolver"
);
}
#[test]
fn borrowed_scope_preserves_inline_overflow_and_replacement_semantics() {
let values = [
Value::String("a".to_string()),
Value::String("b".to_string()),
Value::String("c".to_string()),
Value::String("d".to_string()),
Value::String("e".to_string()),
Value::String("f".to_string()),
Value::String("replacement".to_string()),
];
let mut scope = BorrowedScope::default();
for (index, name) in ["a", "b", "c", "d", "e", "f"].iter().enumerate() {
assert!(scope.insert(name, &values[index]).is_none());
}
assert_eq!(scope.inline_len, INLINE_SCOPE_SLOTS);
assert_eq!(scope.overflow.len(), 2);
assert_eq!(scope.get("a"), Some(&values[0]));
assert_eq!(scope.get("f"), Some(&values[5]));
assert_eq!(scope.insert("a", &values[6]), Some(&values[0]));
assert_eq!(scope.get("a"), Some(&values[6]));
assert_eq!(scope.remove("b"), Some(&values[1]));
assert!(scope.get("b").is_none());
assert_eq!(scope.get("f"), Some(&values[5]));
scope.clear();
assert!(scope.get("a").is_none());
assert!(scope.overflow.is_empty());
}
struct TestWriter {
content: RefCell<String>,
ended: RefCell<bool>,
}
impl TestWriter {
fn new() -> Self {
Self {
content: RefCell::new(String::new()),
ended: RefCell::new(false),
}
}
fn get_content(&self) -> String {
self.content.borrow().clone()
}
fn is_ended(&self) -> bool {
*self.ended.borrow()
}
}
impl ResponseWriter for TestWriter {
fn write(&mut self, content: &str) -> Result<()> {
self.content.borrow_mut().push_str(content);
Ok(())
}
fn end(&mut self) -> Result<()> {
*self.ended.borrow_mut() = true;
Ok(())
}
}
#[test]
fn generated_string_writer_methods_preserve_exact_attribute_output() {
struct AttributeWriter {
output: String,
}
impl ResponseWriter for AttributeWriter {
fn write(&mut self, content: &str) -> Result<()> {
self.output.push_str(content);
Ok(())
}
crate::string_response_writer_methods!(output);
fn end(&mut self) -> Result<()> {
Ok(())
}
}
let mut writer = AttributeWriter {
output: String::new(),
};
write_attr(&mut writer, "data-id", "42")
.unwrap_or_else(|error| panic!("attribute write failed: {error}"));
writer
.write_boolean_attribute("disabled")
.unwrap_or_else(|error| panic!("boolean attribute write failed: {error}"));
assert_eq!(writer.output, " data-id=\"42\" disabled");
}
#[test]
fn prepared_graph_resolves_targets_and_canonical_component_prop_names() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment {
fragment: Some(Fragment::Attribute(WebUIFragmentAttribute {
name: ":data-title".to_string(),
value: "source".to_string(),
complex: true,
attr_start: true,
..Default::default()
})),
},
WebUIFragment::component("prepared-child"),
],
contains_boundary: false,
},
);
fragments.insert(
"prepared-child".to_string(),
FragmentList {
fragments: vec![WebUIFragment::signal("dataTitle", false)],
contains_boundary: false,
},
);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let resolved = protocol.render_fragments().resolve(protocol.protocol());
let entry = resolved
.list_by_id("index.html")
.unwrap_or_else(|| panic!("entry render plan should exist"));
let child_index = protocol
.render_fragments()
.index("prepared-child")
.unwrap_or_else(|| panic!("component target should be indexed"));
assert_eq!(entry.component_attr_name(0), Some("dataTitle"));
assert_eq!(entry.target(1), Some(child_index));
assert!(!entry.has_routes);
let source_entry = protocol
.protocol()
.fragments
.get("index.html")
.unwrap_or_else(|| panic!("source entry should exist"));
assert!(std::ptr::eq(
entry.fragments.as_ptr(),
source_entry.fragments.as_ptr()
));
let state = test_json!({"source": "Prepared"});
let options = RenderOptions::new("index.html", "/");
for handler in [
WebUIHandler::new(),
WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
}),
] {
let mut writer = TestWriter::new();
assert!(handler
.render(&protocol, &state, &options, &mut writer)
.is_ok());
assert!(writer.get_content().contains("Prepared"));
}
}
#[test]
fn prepared_graph_records_route_presence_per_fragment_list() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route("/", "dash-page")],
contains_boundary: false,
},
);
fragments.insert(
"dash-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("dashboard")],
contains_boundary: false,
},
);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let resolved = protocol.render_fragments().resolve(protocol.protocol());
let entry = resolved
.list_by_id("index.html")
.unwrap_or_else(|| panic!("entry render plan should exist"));
let page = resolved
.list_by_id("dash-page")
.unwrap_or_else(|| panic!("page render plan should exist"));
assert!(entry.has_routes);
assert!(!page.has_routes);
}
#[test]
fn prepared_prop_names_match_attribute_to_camel_exactly() {
let names = [
"data-title",
":data-title",
"aria-labelledby",
"aria-valuemax",
"readonly",
"tabindex",
"class",
"for",
"maxlength",
"novalidate",
"already-camel",
"trailing-",
"-leading",
"multi-part-attribute-name",
"single",
"",
];
for name in names {
let stripped = name.strip_prefix(':').unwrap_or(name);
assert_eq!(
component_attr_name(name),
webui_protocol::attrs::attribute_to_camel(stripped),
"prepared name diverged for `{name}`"
);
}
}
#[test]
fn prepared_component_prop_names_match_per_branch_stripping() {
let complex = WebUIFragmentAttribute {
name: "::data-title".to_string(),
value: "source".to_string(),
complex: true,
..Default::default()
};
assert_eq!(component_attr_source(&complex), ":data-title");
assert_eq!(
component_attr_name(component_attr_source(&complex)),
"dataTitle"
);
let raw = WebUIFragmentAttribute {
raw_value: true,
..complex.clone()
};
assert_eq!(component_attr_source(&raw), "::data-title");
let templated = WebUIFragmentAttribute {
template: "tpl".to_string(),
..complex.clone()
};
assert_eq!(component_attr_source(&templated), "::data-title");
let empty_value = WebUIFragmentAttribute {
value: String::new(),
..complex.clone()
};
assert_eq!(component_attr_source(&empty_value), "::data-title");
let ordinary = WebUIFragmentAttribute {
name: ":data-title".to_string(),
..complex
};
assert_eq!(component_attr_source(&ordinary), "data-title");
assert_eq!(
component_attr_name(component_attr_source(&ordinary)),
"dataTitle"
);
}
#[test]
fn render_slots_match_continuation_slots() {
let mut fragments = HashMap::new();
for id in [
"index.html",
"zeta-last",
"alpha-first",
"middle-one",
"middle-two",
] {
fragments.insert(
id.to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(id)],
contains_boundary: false,
},
);
}
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let index = protocol.render_fragments();
for slot in 0..u32::try_from(protocol.protocol().fragments.len())
.unwrap_or_else(|_| panic!("fragment count fits in u32"))
{
let id = protocol
.fragment_id(slot)
.unwrap_or_else(|| panic!("slot {slot} should resolve to an ID"));
assert_eq!(
index.index(id),
Some(slot as usize),
"render slot must equal continuation slot for `{id}`"
);
assert_eq!(protocol.fragment_slot(id), Some(slot));
}
}
#[test]
fn prepared_graph_reports_missing_fragments_without_panicking() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("absent-child")],
contains_boundary: false,
},
);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let resolved = protocol.render_fragments().resolve(protocol.protocol());
let entry = resolved
.list_by_id("index.html")
.unwrap_or_else(|| panic!("entry render plan should exist"));
assert_eq!(entry.target(0), None);
assert!(resolved.list_by_id("absent-child").is_none());
assert!(resolved.list(usize::MAX).is_none());
let state = test_json!({});
let options = RenderOptions::new("index.html", "/");
let mut writer = TestWriter::new();
match WebUIHandler::new().render(&protocol, &state, &options, &mut writer) {
Err(HandlerError::MissingFragment(id)) => assert_eq!(id, "absent-child"),
other => panic!("expected a missing fragment error, got {other:?}"),
}
let mut writer = TestWriter::new();
match WebUIHandler::new().render(
&protocol,
&state,
&RenderOptions::new("absent-entry", "/"),
&mut writer,
) {
Err(HandlerError::MissingFragment(id)) => assert_eq!(id, "absent-entry"),
other => panic!("expected a missing entry error, got {other:?}"),
}
assert!(protocol.has_fragment("index.html"));
assert!(!protocol.has_fragment("absent-entry"));
}
#[test]
fn prepared_graph_lazily_borrows_large_protocol_fragment_lists() {
let list_count = INLINE_RENDER_FRAGMENT_LISTS * 2 + 1;
let mut fragments = HashMap::with_capacity(list_count);
for index in 0..list_count {
fragments.insert(
format!("fragment-{index}"),
FragmentList {
fragments: vec![WebUIFragment::raw("content")],
contains_boundary: false,
},
);
}
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let resolved = protocol.render_fragments().resolve(protocol.protocol());
assert!(resolved.cache[0].get().is_none());
assert!(resolved.spilled.get().is_none());
assert!(resolved.list(0).is_some());
assert!(resolved.cache[0].get().is_some());
assert!(resolved.spilled.get().is_none());
assert!(resolved.list(INLINE_RENDER_FRAGMENT_LISTS).is_some());
let spilled = resolved
.spilled
.get()
.expect("a slot past the inline cache should allocate the spill region");
assert_eq!(spilled.len(), list_count - INLINE_RENDER_FRAGMENT_LISTS);
assert!(spilled[0].get().is_some());
assert!(resolved.list(list_count - 1).is_some());
for (id, source) in &protocol.protocol().fragments {
let prepared = resolved
.list_by_id(id)
.unwrap_or_else(|| panic!("render plan should contain {id}"));
assert!(std::ptr::eq(
prepared.fragments.as_ptr(),
source.fragments.as_ptr()
));
}
}
#[test]
fn test_handle_raw() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("Hello, WebUI!")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
assert!(
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer
)
.is_ok(),
"Failed to handle raw protocol"
);
assert_eq!(writer.get_content(), "Hello, WebUI!");
assert!(writer.is_ended());
}
#[test]
fn test_handle_signal() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Hello, "),
WebUIFragment::signal("name", false),
WebUIFragment::raw("!"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"name": "WebUI"});
let mut writer = TestWriter::new();
assert!(
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer
)
.is_ok(),
"Failed to handle signal protocol"
);
assert_eq!(writer.get_content(), "Hello, WebUI!");
assert!(writer.is_ended());
}
#[test]
fn test_handle_for_loop() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("People: "),
WebUIFragment::for_loop("person", "people", "person-item"),
],
contains_boundary: false,
},
);
fragments.insert(
"person-item".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::signal("person.name", false),
WebUIFragment::raw(", "),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"people": [
{"name": "Alice"},
{"name": "Bob"},
{"name": "Charlie"}
]
});
let mut writer = TestWriter::new();
assert!(
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer
)
.is_ok(),
"Failed to handle for loop protocol"
);
assert_eq!(writer.get_content(), "People: Alice, Bob, Charlie, ");
assert!(writer.is_ended());
}
#[test]
fn test_handle_if_condition() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Status: "),
WebUIFragment::if_cond(
webui_protocol::ConditionExpr::identifier("isActive"),
"active-content",
),
WebUIFragment::raw("End"),
],
contains_boundary: false,
},
);
fragments.insert(
"active-content".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("Active")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state_true = test_json!({"isActive": true});
let mut writer_true = TestWriter::new();
assert!(
handle(
&protocol,
&state_true,
&RenderOptions::new("index.html", "/"),
&mut writer_true
)
.is_ok(),
"Failed to handle if condition (true case)"
);
assert_eq!(writer_true.get_content(), "Status: ActiveEnd");
assert!(writer_true.is_ended());
let state_false = test_json!({"isActive": false});
let mut writer_false = TestWriter::new();
assert!(
handle(
&protocol,
&state_false,
&RenderOptions::new("index.html", "/"),
&mut writer_false
)
.is_ok(),
"Failed to handle if condition (false case)"
);
assert_eq!(writer_false.get_content(), "Status: End");
assert!(writer_false.is_ended());
}
#[test]
fn missing_identifier_is_falsy_before_negation_in_global_and_loop_scopes() {
let source = r#"<if condition="missingTopLevel">top-positive</if><if condition="!missingTopLevel">top-negated</if><for each="item in items"><if condition="item.searchPresentation"><mark>{{item.label}}</mark></if><if condition="!item.searchPresentation"><span>{{item.label}}</span></if></for>"#;
let mut parser = HtmlParser::new();
parser
.parse("index.html", source)
.expect("parse missing-path condition fixture");
let protocol = WebUIProtocol::new(parser.into_fragment_records());
let state = test_json!({
"items": [
{"label": "Normal"},
{"label": "Search", "searchPresentation": true}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.expect("render missing-path condition fixture");
assert_eq!(
writer.get_content(),
"top-negated<span>Normal</span><mark>Search</mark>"
);
}
#[test]
fn native_dynamic_attribute_does_not_leak_into_next_component() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "nativeTitle".into(),
..Default::default()
},
)),
},
WebUIFragment::raw("></div><my-comp>"),
WebUIFragment::component("my-comp"),
WebUIFragment::raw("</my-comp>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-comp".to_string(),
FragmentList {
fragments: vec![WebUIFragment::signal("title", false)],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"nativeTitle": "native",
"title": "global"
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap_or_else(|error| panic!("render failed: {error}"));
assert_eq!(
writer.get_content(),
"<div title=\"native\"></div><my-comp>global</my-comp>"
);
}
#[test]
fn test_handle_component() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Component: "),
WebUIFragment::component("my-component"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Component Content</div>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
assert!(
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer
)
.is_ok(),
"Failed to handle component protocol"
);
assert_eq!(
writer.get_content(),
"Component: <div>Component Content</div>"
);
assert!(writer.is_ended());
}
#[test]
fn test_missing_fragment() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("missing-component")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
let result = handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
);
assert!(result.is_err());
if let Err(HandlerError::MissingFragment(fragment_id)) = result {
assert_eq!(fragment_id, "missing-component");
} else {
panic!("Expected MissingFragment error");
}
}
#[test]
fn test_missing_signal_renders_empty() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Hello, "),
WebUIFragment::signal("missing_field", false),
WebUIFragment::raw("!"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
assert!(
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer
)
.is_ok(),
"Missing signal should not produce an error"
);
assert_eq!(writer.get_content(), "Hello, !");
assert!(writer.is_ended());
}
#[test]
fn test_boolean_attr_true() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<button"),
WebUIFragment::attribute_boolean(
"disabled",
ConditionExpr::identifier("isDisabled"),
),
WebUIFragment::raw(">Click</button>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"isDisabled": true});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<button disabled>Click</button>");
}
#[test]
fn test_boolean_attr_false() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<button"),
WebUIFragment::attribute_boolean(
"disabled",
ConditionExpr::identifier("isDisabled"),
),
WebUIFragment::raw(">Click</button>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"isDisabled": false});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<button>Click</button>");
}
#[test]
fn test_boolean_attr_missing() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input type=\"checkbox\""),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input type=\"checkbox\">");
}
#[test]
fn test_boolean_attr_multiple() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input type=\"checkbox\""),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::attribute_boolean(
"disabled",
ConditionExpr::identifier("disabled"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"checked": true, "disabled": false});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input type=\"checkbox\" checked>");
}
#[test]
fn test_attribute_with_value() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute("value", "inputValue"),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"inputValue": "Hello"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input value=\"Hello\">");
}
#[test]
fn test_attribute_with_falsy_numeric() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div name=\"test\""),
WebUIFragment::attribute("handle", "number"),
WebUIFragment::raw("></div>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"number": 0});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div name=\"test\" handle=\"0\"></div>"
);
}
#[test]
fn test_attribute_array_value_is_escaped() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<a"),
WebUIFragment::attribute("href", "value"),
WebUIFragment::raw(">demo</a>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"value": ["\" autofocus onfocus=alert(1) x=\""]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let content = writer.get_content();
assert!(
content.contains("""),
"Double quotes inside attribute value must be escaped: {content}"
);
assert_eq!(
content.matches("=\"").count(),
1,
"Only one attribute assignment expected: {content}"
);
}
#[test]
fn test_attribute_object_value_is_escaped() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div"),
WebUIFragment::attribute("data-cfg", "cfg"),
WebUIFragment::raw("></div>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"cfg": {"key": "\" onfocus=alert(1) x=\""}});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let content = writer.get_content();
assert!(
content.contains("""),
"Double quotes inside attribute value must be escaped: {content}"
);
assert_eq!(
content.matches("=\"").count(),
1,
"Only one attribute assignment expected: {content}"
);
}
#[test]
fn test_mixed_attribute_template() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_template("value", "attr-1"),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
fragments.insert(
"attr-1".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("hello "),
WebUIFragment::signal("item", false),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"item": "world"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input value=\"hello world\">");
}
#[test]
fn test_raw_signal_not_escaped() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::signal("html", false),
WebUIFragment::signal("html", true),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"html": "<strong>hi</strong>"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<strong>hi</strong><strong>hi</strong>"
);
}
fn render_named_for(template: &str, state: &Value) -> String {
let mut parser = HtmlParser::with_options(DomStrategy::Light);
parser.parse("index.html", template).unwrap();
let protocol = Protocol::new(WebUIProtocol::new(parser.into_fragment_records()));
let mut writer = TestWriter::new();
WebUIHandler::new()
.render(
&protocol,
state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
writer.get_content()
}
#[test]
fn named_for_renders_the_issue_tree_and_restores_shadowed_items() {
let source = r#"<ul><for each="child in items" id="tree-item"><li>{{child.name}}</li><if condition="{{child.children}}"><ul><for id="tree-item" each="child in child.children" /></ul></if><i>{{child.name}}</i></for></ul><b>{{child.name}}</b>"#;
let state = test_json!({
"child": { "name": "global" },
"items": [
{ "name": "Colors", "children": [{ "name": "Ali" }, { "name": "Alice" }, { "name": "Bob" }] },
{ "name": "Name", "children": [] },
{ "name": "Hobbies", "children": [{ "name": "Sports", "children": [{ "name": "Futbol" }, { "name": "Cricket" }] }] }
]
});
assert_eq!(render_named_for(source, &state), "<ul><li>Colors</li><ul><li>Ali</li><i>Ali</i><li>Alice</li><i>Alice</i><li>Bob</li><i>Bob</i></ul><i>Colors</i><li>Name</li><i>Name</i><li>Hobbies</li><ul><li>Sports</li><ul><li>Futbol</li><i>Futbol</i><li>Cricket</li><i>Cricket</i></ul><i>Sports</i></ul><i>Hobbies</i></ul><b>global</b>");
}
#[test]
fn named_for_missing_and_empty_children_stop_recursion_without_an_if() {
let source = r#"<for id="tree" each="item in items">{{item.name}}<for id="tree" each="item in item.children" />{{item.name}}</for>"#;
let state = test_json!({
"items": [{ "name": "a", "children": [{ "name": "b" }, { "name": "c", "children": [] }] }]
});
assert_eq!(render_named_for(source, &state), "abbcca");
assert_eq!(render_named_for(source, &test_json!({"items": []})), "");
}
#[test]
fn named_for_forward_and_mutual_references_follow_finite_data() {
let source = r#"<for id="a" each="item in items" /><for id="a" each="item in empty"><b>{{item.name}}</b><for id="b" each="item in item.children" /></for><for id="b" each="item in empty"><i>{{item.name}}</i><for id="a" each="item in item.children" /></for>"#;
let state = test_json!({
"items": [{ "name": "a", "children": [{ "name": "b", "children": [{ "name": "c" }] }] }],
"empty": []
});
assert_eq!(render_named_for(source, &state), "<b>a</b><i>b</i><b>c</b>");
}
#[test]
fn named_for_preserves_enclosing_scope_and_escapes_nested_values() {
let source = r#"<for each="group in groups"><for id="tree" each="item in group.items">{{group.name}}:{{item.name}};<for id="tree" each="item in item.children" /></for></for>"#;
let state = test_json!({
"groups": [
{ "name": "one", "items": [{ "name": "<root>", "children": [{ "name": "&leaf" }] }] },
{ "name": "two", "items": [{ "name": "last" }] }
]
});
assert_eq!(
render_named_for(source, &state),
"one:<root>;one:&leaf;two:last;"
);
}
#[test]
fn named_for_non_array_collection_returns_the_existing_type_error() {
let mut parser = HtmlParser::new();
parser.parse("index.html", r#"<for id="tree" each="item in items"><for id="tree" each="item in item.children" /></for>"#).unwrap();
let protocol = Protocol::new(WebUIProtocol::new(parser.into_fragment_records()));
let mut writer = TestWriter::new();
let error = WebUIHandler::new()
.render(
&protocol,
&test_json!({"items": [{"children": 1}]}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap_err();
assert!(matches!(error, HandlerError::TypeError(_)));
}
#[test]
fn test_nested_for_loop() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outerItem", "outerItems", "outer"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outer".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "inner"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"inner".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<span>Inner</span>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"outerItems": [
{"innerItems": [{"name": "A"}, {"name": "B"}]},
{"innerItems": [{"name": "C"}]}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><div><span>Inner</span><span>Inner</span></div><div><span>Inner</span></div></div>"
);
}
#[test]
fn test_nested_for_with_signals() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("innerItem.name", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"outerItems": [
{"innerItems": [{"name": "Item1"}, {"name": "Item2"}]},
{"innerItems": [{"name": "Item3"}, {"name": "Item4"}]}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><div><span>Item1</span><span>Item2</span></div><div><span>Item3</span><span>Item4</span></div></div>"
);
}
#[test]
fn test_nested_for_with_global_state() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::signal("globalOuter", false),
WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("innerItem.name", false),
WebUIFragment::signal("globalInner", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"globalOuter": "GO",
"globalInner": "GI",
"outerItems": [
{"innerItems": [{"name": "Item1"}, {"name": "Item2"}]},
{"innerItems": [{"name": "Item3"}, {"name": "Item4"}]}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><div>GO<span>Item1GI</span><span>Item2GI</span></div><div>GO<span>Item3GI</span><span>Item4GI</span></div></div>"
);
}
#[test]
fn test_if_in_for_uses_local_state() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop("item", "items", "item-tpl")],
contains_boundary: false,
},
);
fragments.insert(
"item-tpl".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::identifier("item.visible"),
"visible-tpl",
)],
contains_boundary: false,
},
);
fragments.insert(
"visible-tpl".to_string(),
FragmentList {
fragments: vec![WebUIFragment::signal("item.name", false)],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"items": [{"name": "Show", "visible": true}, {"name": "Hide", "visible": false}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "Show");
}
#[test]
fn test_for_if_local_overrides_global() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop("item", "items", "item-tpl")],
contains_boundary: false,
},
);
fragments.insert(
"item-tpl".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::identifier("item.flag"),
"show-tpl",
)],
contains_boundary: false,
},
);
fragments.insert(
"show-tpl".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("yes")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"flag": true, "items": [{"flag": true}, {"flag": false}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "yes");
}
#[test]
fn html_component_inputs_forward_flags_and_aria_without_javascript_metadata() {
let cases = [
(
r#"<my-drawer open aria-label="Canvas information"></my-drawer>"#,
test_json!({}),
r#"<dialog open aria-label="Canvas information"></dialog>"#,
),
(
r#"<my-drawer ?open="{{open}}" aria-label="Canvas information"></my-drawer>"#,
test_json!({"open": true}),
r#"<dialog open aria-label="Canvas information"></dialog>"#,
),
(
r#"<my-drawer ?open="{{open}}" aria-label="Canvas information"></my-drawer>"#,
test_json!({"open": false}),
r#"<dialog aria-label="Canvas information"></dialog>"#,
),
(
r#"<my-drawer aria-label=""></my-drawer>"#,
test_json!({"ariaLabel": "Fallback"}),
r#"<dialog aria-label=""></dialog>"#,
),
(
r#"<my-drawer open aria-label="Canvas & information"></my-drawer>"#,
test_json!({}),
r#"<dialog open aria-label="Canvas & information"></dialog>"#,
),
];
for (input, state, expected) in cases {
let mut parser = HtmlParser::with_options(DomStrategy::Light);
parser
.component_registry_mut()
.register_component(ComponentRegistration::new(
"my-drawer",
r#"<dialog ?open="{{open}}" aria-label="{{ariaLabel}}"></dialog>"#,
None,
true,
))
.unwrap();
parser.parse("index.html", input).unwrap();
let protocol = WebUIProtocol::new(parser.into_fragment_records());
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert!(
writer.get_content().contains(expected),
"{input}: {}",
writer.get_content()
);
}
}
#[test]
fn test_component_attr_state_simple() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-comp"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "Attribute Title".into(),
attr_start: true,
raw_value: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-comp"),
WebUIFragment::raw("</my-comp>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-comp".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"title": "Global Title"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-comp title=\"Attribute Title\"><span>Attribute Title</span></my-comp>"
);
}
#[test]
fn test_component_attr_state_template() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-comp"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
template: "title-attr".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-comp"),
WebUIFragment::raw("</my-comp>"),
],
contains_boundary: false,
},
);
fragments.insert(
"title-attr".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("hello "),
WebUIFragment::signal("item", false),
],
contains_boundary: false,
},
);
fragments.insert(
"my-comp".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"item": "<world>"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-comp title=\"hello <world>\"><span>hello <world></span></my-comp>"
);
}
#[test]
fn test_component_attr_camel_case() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-comp"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "data-title".into(),
template: "dt-attr".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-comp"),
WebUIFragment::raw("</my-comp>"),
],
contains_boundary: false,
},
);
fragments.insert(
"dt-attr".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("prefix "),
WebUIFragment::signal("item", false),
],
contains_boundary: false,
},
);
fragments.insert(
"my-comp".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("dataTitle", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"item": "a&b"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-comp data-title=\"prefix a&b\"><span>prefix a&b</span></my-comp>"
);
}
#[test]
fn test_component_complex_attr() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-comp"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: ":item".into(),
value: "complexItem".into(),
attr_start: true,
complex: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-comp"),
WebUIFragment::raw("</my-comp>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-comp".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("item.foo", false),
WebUIFragment::raw("</span><p>"),
WebUIFragment::signal("item.bar", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"complexItem": {"foo": 1, "bar": "true"}});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-comp><span>1</span><p>true</p></my-comp>"
);
}
#[test]
fn test_component_no_parent_pollution() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<parent"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "var".into(),
value: "var".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("parent"),
WebUIFragment::raw("</parent>"),
],
contains_boundary: false,
},
);
fragments.insert(
"parent".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Before: "),
WebUIFragment::signal("var", false),
WebUIFragment::raw("<child foo"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "var".into(),
value: "replaced".into(),
raw_value: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("child"),
WebUIFragment::raw("Label</child>After: "),
WebUIFragment::signal("var", false),
],
contains_boundary: false,
},
);
fragments.insert(
"child".to_string(),
FragmentList {
fragments: vec![],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"var": "original<&"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<parent var=\"original<&\">Before: original<&<child foo var=\"replaced\">Label</child>After: original<&</parent>"
);
}
#[test]
fn test_component_boolean_attr_state() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-comp"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "disabled".into(),
attr_start: true,
condition_tree: Some(ConditionExpr::identifier("isDisabled")),
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-comp"),
WebUIFragment::raw("</my-comp>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-comp".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::identifier("disabled"),
"show",
)],
contains_boundary: false,
},
);
fragments.insert(
"show".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("disabled!")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"isDisabled": true});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-comp disabled>disabled!</my-comp>"
);
}
fn render_signal(value: &str) -> String {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::signal("v", false)],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"v": value});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
writer.get_content()
}
#[test]
fn test_escape_ampersand() {
assert_eq!(render_signal("&"), "&");
}
#[test]
fn test_escape_less_than() {
assert_eq!(render_signal("<"), "<");
}
#[test]
fn test_escape_greater_than() {
assert_eq!(render_signal(">"), ">");
}
#[test]
fn test_escape_double_quote() {
assert_eq!(render_signal("\""), """);
}
#[test]
fn test_escape_single_quote() {
let result = render_signal("'");
assert_eq!(
result, "'",
"Expected ' for single quote, got: {result}"
);
}
#[test]
fn test_escape_multiple_special_chars() {
let result = render_signal("<script>alert('xss');</script>");
assert!(
result.contains("<") && result.contains(">"),
"Expected escaped HTML, got: {}",
result
);
assert!(
!result.contains("<script>"),
"Should not contain raw <script> tag"
);
}
#[test]
fn test_escape_no_special_chars() {
assert_eq!(render_signal("Hello World"), "Hello World");
}
#[test]
fn test_escape_empty_string() {
assert_eq!(render_signal(""), "");
}
#[test]
fn test_escape_special_at_beginning() {
let result = render_signal("<Hello");
assert!(
result.starts_with("<"),
"Expected < at start, got: {}",
result
);
}
#[test]
fn test_escape_special_at_end() {
let result = render_signal("Hello>");
assert!(
result.ends_with(">"),
"Expected > at end, got: {}",
result
);
}
#[test]
fn test_escape_special_in_middle() {
let result = render_signal("Hel&lo");
assert!(
result.contains("&"),
"Expected & in middle, got: {}",
result
);
}
#[test]
fn test_boolean_attr_truthy_values() {
{
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"checked": 1});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input checked>");
}
{
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"checked": "yes"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input checked>");
}
{
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"checked": {}});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input>");
}
{
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"checked": "false"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input checked>");
}
}
#[test]
fn test_boolean_attr_falsy_values() {
{
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"checked": 0});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input>");
}
{
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"checked": ""});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input>");
}
{
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"checked": false});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input>");
}
{
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<input"),
WebUIFragment::attribute_boolean(
"checked",
ConditionExpr::identifier("checked"),
),
WebUIFragment::raw(">"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<input>");
}
}
#[test]
fn test_boolean_attr_expression_true() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<button"),
WebUIFragment::attribute_boolean(
"disabled",
ConditionExpr::predicate("itemCount", ComparisonOperator::Equal, "5"),
),
WebUIFragment::raw(">Click</button>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"itemCount": 5});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<button disabled>Click</button>");
}
#[test]
fn test_boolean_attr_expression_false() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<button"),
WebUIFragment::attribute_boolean(
"disabled",
ConditionExpr::predicate("itemCount", ComparisonOperator::Equal, "5"),
),
WebUIFragment::raw(">Click</button>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"itemCount": 3});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<button>Click</button>");
}
#[test]
fn test_nested_component_attr_capture() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<parent-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
template: "parent-title".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("parent-component"),
WebUIFragment::raw("</parent-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"parent-title".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Hello "),
WebUIFragment::signal("who", false),
],
contains_boundary: false,
},
);
fragments.insert(
"parent-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<child-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
template: "child-title".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("child-component"),
WebUIFragment::raw("</child-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"child-title".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Child of "),
WebUIFragment::signal("title", false),
],
contains_boundary: false,
},
);
fragments.insert(
"child-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"who": "<world>"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<parent-component title=\"Hello <world>\"><child-component title=\"Child of Hello <world>\"><span>Child of Hello <world></span></child-component></parent-component>"
);
}
#[test]
fn test_grandchild_attr_propagation() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<parent-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
template: "p-title".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("parent-component"),
WebUIFragment::raw("</parent-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"p-title".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("P:"), WebUIFragment::signal("p", false)],
contains_boundary: false,
},
);
fragments.insert(
"parent-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<child-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
template: "c-title".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("child-component"),
WebUIFragment::raw("</child-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"c-title".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("C("),
WebUIFragment::signal("title", false),
WebUIFragment::raw(")-"),
WebUIFragment::signal("cExtra", false),
],
contains_boundary: false,
},
);
fragments.insert(
"child-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<grandchild-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "title".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("grandchild-component"),
WebUIFragment::raw("</grandchild-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"grandchild-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"p": "<p>", "cExtra": "x&y"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<parent-component title=\"P:<p>\"><child-component title=\"C(P:<p>)-x&y\"><grandchild-component title=\"C(P:<p>)-x&y\"><span>C(P:<p>)-x&y</span></grandchild-component></child-component></parent-component>"
);
}
#[test]
fn test_for_loop_component_attr() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<parent-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
template: "parent-title-loop".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("parent-component"),
WebUIFragment::raw("</parent-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"parent-title-loop".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Parent:"),
WebUIFragment::signal("who", false),
],
contains_boundary: false,
},
);
fragments.insert(
"parent-component".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop("item", "items", "child-loop")],
contains_boundary: false,
},
);
fragments.insert(
"child-loop".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<child-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
template: "child-title-loop".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("child-component"),
WebUIFragment::raw("</child-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"child-title-loop".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("Hi "),
WebUIFragment::signal("item.name", false),
WebUIFragment::raw(" / "),
WebUIFragment::signal("title", false),
],
contains_boundary: false,
},
);
fragments.insert(
"child-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"who": "Bob", "items": [{"name": "A<1>"}, {"name": "B&2"}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<parent-component title=\"Parent:Bob\"><child-component title=\"Hi A<1> / Parent:Bob\"><span>Hi A<1> / Parent:Bob</span></child-component><child-component title=\"Hi B&2 / Parent:Bob\"><span>Hi B&2 / Parent:Bob</span></child-component></parent-component>"
);
}
#[test]
fn test_multiple_template_attrs() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
template: "attr-title".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "data-title".into(),
template: "attr-data-title".into(),
attr_start: false,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "aria-label".into(),
template: "attr-aria-label".into(),
attr_start: false,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</my-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"attr-title".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("T:"), WebUIFragment::signal("t", false)],
contains_boundary: false,
},
);
fragments.insert(
"attr-data-title".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("D:"), WebUIFragment::signal("d", false)],
contains_boundary: false,
},
);
fragments.insert(
"attr-aria-label".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("A:"), WebUIFragment::signal("a", false)],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("|"),
WebUIFragment::signal("dataTitle", false),
WebUIFragment::raw("|"),
WebUIFragment::signal("ariaLabel", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"t": "<t&1>", "d": "d<2>", "a": "a&3"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-component title=\"T:<t&1>\" data-title=\"D:d<2>\" aria-label=\"A:a&3\"><span>T:<t&1>|D:d<2>|A:a&3</span></my-component>"
);
}
#[test]
fn test_attr_priority_over_global() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "Attribute Title".into(),
raw_value: true,
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</my-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"title": "Global Title"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-component title=\"Attribute Title\"><span>Attribute Title</span></my-component>"
);
}
#[test]
fn test_attr_priority_over_local_and_global() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop("item", "items", "loop")],
contains_boundary: false,
},
);
fragments.insert(
"loop".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "Attribute Title".into(),
raw_value: true,
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</my-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"title": "Global Title", "items": [{"title": "Local Title"}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-component title=\"Attribute Title\"><span>Attribute Title</span></my-component>"
);
}
#[test]
fn test_boolean_attr_first_component_attr() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "disabled".into(),
attr_start: true,
condition_tree: Some(ConditionExpr::identifier("isDisabled")),
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "label".into(),
value: "Component Label".into(),
raw_value: true,
attr_start: false,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</my-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::if_cond(
ConditionExpr::identifier("disabled"),
"disabledTemplate",
),
WebUIFragment::raw("<span>"),
WebUIFragment::signal("label", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
fragments.insert(
"disabledTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Disabled</div>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"isDisabled": true});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-component disabled label=\"Component Label\"><div>Disabled</div><span>Component Label</span></my-component>"
);
}
#[test]
fn test_hyphenated_attr_camelcase() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "key-hyphen".into(),
value: "Local Value".into(),
raw_value: true,
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</my-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("keyHyphen", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"keyHyphen": "Global Value"});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-component key-hyphen=\"Local Value\"><span>Local Value</span></my-component>"
);
}
#[test]
fn test_skipped_component_attrs() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<test-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "class".into(),
value: "skippedClass".into(),
attr_start: true,
attr_skip: true,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "style".into(),
value: "skippedStyle".into(),
attr_skip: true,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "role".into(),
value: "skippedRole".into(),
attr_skip: true,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "data-testid".into(),
value: "skippedDataTestid".into(),
attr_skip: true,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "aria-label".into(),
value: "skippedAriaLabel".into(),
attr_skip: true,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "title".into(),
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("test-component"),
WebUIFragment::raw("</test-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"test-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("class", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("style", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("role", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("dataTestid", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("ariaLabel", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"title": "Hello",
"skippedClass": "my-class",
"skippedStyle": "color:red",
"skippedRole": "button",
"skippedDataTestid": "test-id",
"skippedAriaLabel": "label-text"
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<test-component class=\"my-class\" style=\"color:red\" role=\"button\" data-testid=\"test-id\" aria-label=\"label-text\" title=\"Hello\"><span>Hello-----</span></test-component>"
);
}
#[test]
fn test_attr_inherit_parent_to_child() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<parent-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "Parent Title".into(),
raw_value: true,
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("parent-component"),
WebUIFragment::raw("</parent-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"parent-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<h1>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</h1><child-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "title".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("child-component"),
WebUIFragment::raw("</child-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"child-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<h2>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</h2>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<parent-component title=\"Parent Title\"><h1>Parent Title</h1><child-component title=\"Parent Title\"><h2>Parent Title</h2></child-component></parent-component>"
);
}
#[test]
fn test_attr_inherit_deep() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<parent-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "Parent Title".into(),
raw_value: true,
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("parent-component"),
WebUIFragment::raw("</parent-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"parent-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<child-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "Child Title".into(),
raw_value: true,
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("child-component"),
WebUIFragment::raw("</child-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"child-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<grandchild-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "title".into(),
value: "title".into(),
attr_start: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("grandchild-component"),
WebUIFragment::raw("</grandchild-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"grandchild-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<h3>"),
WebUIFragment::signal("title", false),
WebUIFragment::raw("</h3>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<parent-component title=\"Parent Title\"><child-component title=\"Child Title\"><grandchild-component title=\"Child Title\"><h3>Child Title</h3></grandchild-component></child-component></parent-component>"
);
}
#[test]
fn test_complex_attr_access() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: ":item".into(),
value: "complexItem".into(),
attr_start: true,
complex: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</my-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("item.foo", false),
WebUIFragment::raw("</span><p>"),
WebUIFragment::signal("item.bar", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"complexItem": {"foo": 1, "bar": "true"}});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-component><span>1</span><p>true</p></my-component>"
);
}
#[test]
fn test_complex_attr_for_loop() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop(
"item",
"list.items",
"listTemplate",
)],
contains_boundary: false,
},
);
fragments.insert(
"listTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: ":item".into(),
value: "item".into(),
attr_start: true,
complex: true,
..Default::default()
},
)),
},
WebUIFragment::component("item_component"),
],
contains_boundary: false,
},
);
fragments.insert(
"item_component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("item.name", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"list": {"items": [{"name": "Alice"}, {"name": "Bob"}]}});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.get_content(), "<span>Alice</span><span>Bob</span>");
}
#[test]
fn test_complex_attr_nested_for() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop(
"outer",
"data.outer",
"outerTemplate",
)],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop(
"middle",
"outer.middle",
"middleTemplate",
)],
contains_boundary: false,
},
);
fragments.insert(
"middleTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop(
"inner",
"middle.inner",
"innerTemplate",
)],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<card"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: ":outer".into(),
value: "outer".into(),
attr_start: true,
complex: true,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: ":middle".into(),
value: "middle".into(),
attr_start: false,
complex: true,
..Default::default()
},
)),
},
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: ":inner".into(),
value: "inner".into(),
attr_start: false,
complex: true,
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("card_component"),
WebUIFragment::raw("</card>"),
],
contains_boundary: false,
},
);
fragments.insert(
"card_component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<p>"),
WebUIFragment::signal("outer.label", false),
WebUIFragment::raw(" / "),
WebUIFragment::signal("middle.label", false),
WebUIFragment::raw(" / "),
WebUIFragment::signal("inner.label", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"data": {"outer": [
{"label": "Outer1", "middle": [{"label": "Middle1", "inner": [{"label": "Inner1A"}, {"label": "Inner1B"}]}]},
{"label": "Outer2", "middle": [{"label": "Middle2", "inner": [{"label": "Inner2A"}]}]}
]}});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<card><p>Outer1 / Middle1 / Inner1A</p></card><card><p>Outer1 / Middle1 / Inner1B</p></card><card><p>Outer2 / Middle2 / Inner2A</p></card>"
);
}
#[test]
fn test_bool_component_state_true() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "disabled".into(),
attr_start: true,
condition_tree: Some(ConditionExpr::identifier("isDisabled")),
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</my-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::if_cond(
ConditionExpr::identifier("disabled"),
"disabledTemplate",
),
WebUIFragment::if_cond(
ConditionExpr::negated(ConditionExpr::identifier("disabled")),
"enabledTemplate",
),
],
contains_boundary: false,
},
);
fragments.insert(
"disabledTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<span>Disabled</span>")],
contains_boundary: false,
},
);
fragments.insert(
"enabledTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<span>Enabled</span>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"isDisabled": true});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-component disabled><span>Disabled</span></my-component>"
);
}
#[test]
fn test_bool_component_state_false() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<my-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "disabled".into(),
attr_start: true,
condition_tree: Some(ConditionExpr::identifier("isDisabled")),
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</my-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::if_cond(
ConditionExpr::identifier("disabled"),
"disabledTemplate",
),
WebUIFragment::if_cond(
ConditionExpr::negated(ConditionExpr::identifier("disabled")),
"enabledTemplate",
),
],
contains_boundary: false,
},
);
fragments.insert(
"disabledTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<span>Disabled</span>")],
contains_boundary: false,
},
);
fragments.insert(
"enabledTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<span>Enabled</span>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"isDisabled": false});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<my-component><span>Enabled</span></my-component>"
);
}
#[test]
fn test_bool_component_state_forward() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<parent-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "disabled".into(),
attr_start: true,
condition_tree: Some(ConditionExpr::identifier("isDisabled")),
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("parent-component"),
WebUIFragment::raw("</parent-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"parent-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::if_cond(
ConditionExpr::identifier("disabled"),
"parentDisabledTemplate",
),
WebUIFragment::raw("<child-component"),
WebUIFragment {
fragment: Some(web_ui_fragment::Fragment::Attribute(
WebUIFragmentAttribute {
name: "disabled".into(),
attr_start: true,
condition_tree: Some(ConditionExpr::identifier("disabled")),
..Default::default()
},
)),
},
WebUIFragment::raw(">"),
WebUIFragment::component("child-component"),
WebUIFragment::raw("</child-component>"),
],
contains_boundary: false,
},
);
fragments.insert(
"parentDisabledTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Parent Disabled</div>")],
contains_boundary: false,
},
);
fragments.insert(
"child-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::if_cond(
ConditionExpr::identifier("disabled"),
"childDisabledTemplate",
),
WebUIFragment::if_cond(
ConditionExpr::negated(ConditionExpr::identifier("disabled")),
"childEnabledTemplate",
),
],
contains_boundary: false,
},
);
fragments.insert(
"childDisabledTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Child Disabled</div>")],
contains_boundary: false,
},
);
fragments.insert(
"childEnabledTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Child Enabled</div>")],
contains_boundary: false,
},
);
{
let protocol = WebUIProtocol::new(fragments.clone());
let state = test_json!({"isDisabled": true});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<parent-component disabled><div>Parent Disabled</div><child-component disabled><div>Child Disabled</div></child-component></parent-component>"
);
}
{
let protocol = WebUIProtocol::new(fragments.clone());
let state = test_json!({"isDisabled": false});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<parent-component><child-component><div>Child Enabled</div></child-component></parent-component>"
);
}
}
#[test]
fn test_component_with_template() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<custom-element>"),
WebUIFragment::component("custom-element"),
WebUIFragment::raw("</custom-element>"),
],
contains_boundary: false,
},
);
fragments.insert(
"custom-element".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Custom Element</div>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<custom-element><div>Custom Element</div></custom-element>"
);
assert!(writer.is_ended());
}
#[test]
fn test_component_with_slots() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<custom-element appearance=\"subtle\">"),
WebUIFragment::component("custom-element"),
WebUIFragment::raw("Hello World</custom-element>"),
],
contains_boundary: false,
},
);
fragments.insert(
"custom-element".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<slot></slot>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<custom-element appearance=\"subtle\"><slot></slot>Hello World</custom-element>"
);
assert!(writer.is_ended());
}
#[test]
fn test_multiple_nested_components() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "templateRepeat"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"custom-button".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<slot></slot>")],
contains_boundary: false,
},
);
fragments.insert(
"custom-element".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<custom-child>"),
WebUIFragment::component("custom-child"),
WebUIFragment::raw("</custom-child><slot></slot>"),
],
contains_boundary: false,
},
);
fragments.insert(
"custom-child".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>Hello World!</h1>")],
contains_boundary: false,
},
);
fragments.insert(
"templateRepeat".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<custom-element>"),
WebUIFragment::component("custom-element"),
WebUIFragment::raw("<custom-button>"),
WebUIFragment::component("custom-button"),
WebUIFragment::raw("Ok</custom-button></custom-element>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"items": [{"name": "Item1"}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><custom-element><custom-child><h1>Hello World!</h1></custom-child><slot></slot><custom-button><slot></slot>Ok</custom-button></custom-element></div>"
);
assert!(writer.is_ended());
}
#[test]
fn test_if_with_binary_expression() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::if_cond(
ConditionExpr::predicate("x", ComparisonOperator::GreaterThan, "5"),
"if-1",
),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"if-1".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<span>If 1</span>")],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state_true = test_json!({"x": 10});
let mut writer_true = TestWriter::new();
handle(
&protocol,
&state_true,
&RenderOptions::new("index.html", "/"),
&mut writer_true,
)
.unwrap();
assert_eq!(writer_true.get_content(), "<div><span>If 1</span></div>");
let state_false = test_json!({"x": 1});
let mut writer_false = TestWriter::new();
handle(
&protocol,
&state_false,
&RenderOptions::new("index.html", "/"),
&mut writer_false,
)
.unwrap();
assert_eq!(writer_false.get_content(), "<div></div>");
}
#[test]
fn test_for_if_overlapping_local_state() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "template1"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"template1".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::if_cond(ConditionExpr::identifier("item.flag"), "ifBlock"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"ifBlock".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("item.label", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"flag": false,
"items": [
{"label": "A", "flag": true},
{"label": "B", "flag": false},
{"label": "C", "flag": true}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><div><span>A</span></div><div></div><div><span>C</span></div></div>"
);
}
#[test]
fn test_for_if_global_flag_no_effect() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "template1"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"template1".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::if_cond(ConditionExpr::identifier("item.flag"), "ifBlock"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"ifBlock".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("item.label", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"item": {"flag": true},
"items": [
{"label": "A", "flag": false},
{"label": "B", "flag": true},
{"label": "C", "flag": false}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><div></div><div><span>B</span></div><div></div></div>"
);
}
#[test]
fn test_recursive_template_refs() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::for_loop("item", "items", "static")],
contains_boundary: false,
},
);
fragments.insert(
"static".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div expanded=\""),
WebUIFragment::signal("item.expanded", false),
WebUIFragment::raw("\" class=\""),
WebUIFragment::signal("testScenario", false),
WebUIFragment::raw("\"><span>"),
WebUIFragment::signal("item.name", false),
WebUIFragment::raw("</span>"),
WebUIFragment::for_loop("item", "item.children", "static"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"testScenario": "RecursiveTemplatesWithGlobalState",
"items": [
{"name": "A", "expanded": "false", "children": []},
{"name": "B", "expanded": "true", "children": [
{"name": "C", "expanded": "false"},
{"name": "D", "expanded": "false"}
]},
{"name": "E", "expanded": "false"}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div expanded=\"false\" class=\"RecursiveTemplatesWithGlobalState\"><span>A</span></div><div expanded=\"true\" class=\"RecursiveTemplatesWithGlobalState\"><span>B</span><div expanded=\"false\" class=\"RecursiveTemplatesWithGlobalState\"><span>C</span></div><div expanded=\"false\" class=\"RecursiveTemplatesWithGlobalState\"><span>D</span></div></div><div expanded=\"false\" class=\"RecursiveTemplatesWithGlobalState\"><span>E</span></div>"
);
}
#[test]
fn test_component_in_for_no_local_access() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "templateComponent"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"templateComponent".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<component-tag>"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</component-tag>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("name", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"items": [{"name": "Item1"}, {"name": "Item2"}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><component-tag><span></span></component-tag><component-tag><span></span></component-tag></div>"
);
}
#[test]
fn test_nested_for_hierarchical_state() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<section>"),
WebUIFragment::signal("globalPrefix", false),
WebUIFragment::signal("outerItem.outerLabel", false),
WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
WebUIFragment::raw("</section>"),
],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<p>"),
WebUIFragment::signal("globalPrefix", false),
WebUIFragment::signal("outerItem.outerLabel", false),
WebUIFragment::raw(": "),
WebUIFragment::signal("innerItem.innerLabel", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"globalPrefix": "Prefix: ",
"outerItems": [
{"outerLabel": "O1", "innerItems": [{"innerLabel": "I1"}, {"innerLabel": "I2"}]},
{"outerLabel": "O2", "innerItems": [{"innerLabel": "I3"}]}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><section>Prefix: O1<p>Prefix: O1: I1</p><p>Prefix: O1: I2</p></section><section>Prefix: O2<p>Prefix: O2: I3</p></section></div>"
);
}
#[test]
fn test_component_in_for_global_only() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "templateComponent"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"templateComponent".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<component-tag>"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</component-tag>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("name", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("globalSuffix", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state =
test_json!({"globalSuffix": "Global", "items": [{"name": "Item1"}, {"name": "Item2"}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><component-tag><span>-Global</span></component-tag><component-tag><span>-Global</span></component-tag></div>"
);
}
#[test]
fn test_component_no_item_moniker() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "templateComponent"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"templateComponent".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<component-tag>"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</component-tag>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("item.name", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("globalSuffix", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state =
test_json!({"globalSuffix": "Global", "items": [{"name": "Item1"}, {"name": "Item2"}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><component-tag><span>-Global</span></component-tag><component-tag><span>-Global</span></component-tag></div>"
);
}
#[test]
fn test_for_nonqualified_uses_global() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "template1"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"template1".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("name", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({"name": "GlobalName", "items": [{"name": "LocalName1"}, {"name": "LocalName2"}]});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><span>GlobalName</span><span>GlobalName</span></div>"
);
}
#[test]
fn test_nested_for_if_interleaved() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<section>"),
WebUIFragment::signal("globalPrefix", false),
WebUIFragment::signal("outerItem.outerLabel", false),
WebUIFragment::if_cond(
ConditionExpr::identifier("outerItem.include"),
"ifTemplate",
),
WebUIFragment::raw("</section>"),
],
contains_boundary: false,
},
);
fragments.insert(
"ifTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<p>"),
WebUIFragment::signal("globalSuffix", false),
WebUIFragment::raw(": "),
WebUIFragment::signal("innerItem.innerLabel", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"globalPrefix": "Prefix: ",
"globalSuffix": "Suffix",
"outerItems": [
{"outerLabel": "O1", "include": true, "innerItems": [{"innerLabel": "I1"}, {"innerLabel": "I2"}]},
{"outerLabel": "O2", "include": false, "innerItems": [{"innerLabel": "Iignored"}]},
{"outerLabel": "O3", "include": true, "innerItems": [{"innerLabel": "I3"}]}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><section>Prefix: O1<div><p>Suffix: I1</p><p>Suffix: I2</p></div></section><section>Prefix: O2</section><section>Prefix: O3<div><p>Suffix: I3</p></div></section></div>"
);
}
#[test]
fn test_nested_for_if_outer_state() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<section>"),
WebUIFragment::signal("globalPrefix", false),
WebUIFragment::signal("outerItem.label", false),
WebUIFragment::for_loop(
"middleItem",
"outerItem.middleItems",
"middleTemplate",
),
WebUIFragment::raw("</section>"),
],
contains_boundary: false,
},
);
fragments.insert(
"middleTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::if_cond(
ConditionExpr::identifier("outerItem.active"),
"ifTemplate",
),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"ifTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<p>"),
WebUIFragment::signal("middleItem.value", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"globalPrefix": "GP-",
"outerItems": [
{"label": "O1", "active": true, "middleItems": [{"value": "M1"}, {"value": "M2"}]},
{"label": "O2", "active": false, "middleItems": [{"value": "M3"}]},
{"label": "O3", "active": true, "middleItems": [{"value": "M4"}]}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><section>GP-O1<div><p>M1</p></div><div><p>M2</p></div></section><section>GP-O2<div></div></section><section>GP-O3<div><p>M4</p></div></section></div>"
);
}
#[test]
fn test_nested_for_if_inner_state() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outerItem", "outerItems", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<section>"),
WebUIFragment::signal("outerItem.label", false),
WebUIFragment::for_loop("innerItem", "outerItem.innerItems", "innerTemplate"),
WebUIFragment::raw("</section>"),
],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<article>"),
WebUIFragment::if_cond(
ConditionExpr::identifier("innerItem.show"),
"ifTemplate",
),
WebUIFragment::raw("</article>"),
],
contains_boundary: false,
},
);
fragments.insert(
"ifTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<p>"),
WebUIFragment::signal("innerItem.detail", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"outerItems": [
{"label": "Outer1", "innerItems": [{"detail": "Detail1", "show": true}, {"detail": "Detail2", "show": false}]},
{"label": "Outer2", "innerItems": [{"detail": "Detail3", "show": true}]}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><section>Outer1<article><p>Detail1</p></article><article></article></section><section>Outer2<article><p>Detail3</p></article></section></div>"
);
}
#[test]
fn test_for_merge_local_global_monikers() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "template1"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"template1".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("item.name", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("item.globalValue", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("item.localOnly", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("item.otherVal", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"item": {"globalValue": "GLOBAL", "otherVal": "other"},
"items": [
{"name": "Local1", "globalValue": "LOCAL", "localOnly": "Only1"},
{"name": "Local2", "localOnly": "Only2"}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><span>Local1-LOCAL-Only1-other</span><span>Local2-GLOBAL-Only2-other</span></div>"
);
}
#[test]
fn test_component_in_for_global_moniker_shadow() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("item", "items", "templateComponent"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"templateComponent".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<component-tag>"),
WebUIFragment::component("my-component"),
WebUIFragment::raw("</component-tag>"),
],
contains_boundary: false,
},
);
fragments.insert(
"my-component".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<span>"),
WebUIFragment::signal("name", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("item.globalValue", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("localOnly", false),
WebUIFragment::raw("-"),
WebUIFragment::signal("item.otherVal", false),
WebUIFragment::raw("</span>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"item": {"globalValue": "GLOBAL", "otherVal": "other"},
"items": [
{"name": "Local1", "globalValue": "LOCAL", "localOnly": "Only1"},
{"name": "Local2", "localOnly": "Only2"}
]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><component-tag><span>-GLOBAL--other</span></component-tag><component-tag><span>-GLOBAL--other</span></component-tag></div>"
);
}
#[test]
fn test_if_in_nested_for_local_flag() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outer", "list.outer_items", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<section>"),
WebUIFragment::for_loop("inner_item", "outer.inner_items", "innerTemplate"),
WebUIFragment::raw("</section>"),
],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::identifier("inner_item.flag"),
"ifInner",
)],
contains_boundary: false,
},
);
fragments.insert(
"ifInner".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<p>"),
WebUIFragment::signal("inner_item.value", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"list": {"outer_items": [{"inner_items": [{"flag": true, "value": "X"}, {"flag": false, "value": "Y"}]}]},
"inner_item": {"flag": false}
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><section><p>X</p></section></div>"
);
}
#[test]
fn test_if_in_nested_for_global_fallback() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outer", "list.outer_items", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<section>"),
WebUIFragment::for_loop("inner_item", "outer.inner_items", "innerTemplate"),
WebUIFragment::raw("</section>"),
],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::identifier("inner_item.flag"),
"ifInner",
)],
contains_boundary: false,
},
);
fragments.insert(
"ifInner".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<p>"),
WebUIFragment::signal("inner_item.value", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"list": {"outer_items": [{"inner_items": [{"value": "X"}, {"value": "Y"}]}]},
"inner_item": {"flag": true}
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><section><p>X</p><p>Y</p></section></div>"
);
}
#[test]
fn test_if_mixed_for_monikers() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>"),
WebUIFragment::for_loop("outer", "list.outerItems", "outerTemplate"),
WebUIFragment::raw("</div>"),
],
contains_boundary: false,
},
);
fragments.insert(
"outerTemplate".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<section>"),
WebUIFragment::signal("outer.outerLabel", false),
WebUIFragment::for_loop("inner", "outer.innerItems", "innerTemplate"),
WebUIFragment::raw("</section>"),
],
contains_boundary: false,
},
);
fragments.insert(
"innerTemplate".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::compound(
ConditionExpr::identifier("outer.active"),
LogicalOperator::And,
ConditionExpr::predicate(
"inner.value",
ComparisonOperator::GreaterThan,
"globalLimit",
),
),
"ifInner",
)],
contains_boundary: false,
},
);
fragments.insert(
"ifInner".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<p>"),
WebUIFragment::signal("inner.value", false),
WebUIFragment::raw("</p>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"globalLimit": 10,
"list": {"outerItems": [
{"outerLabel": "O1", "active": true, "innerItems": [{"value": 15}, {"value": 8}]},
{"outerLabel": "O2", "active": false, "innerItems": [{"value": 20}]},
{"outerLabel": "O3", "active": true, "innerItems": [{"value": 5}]}
]}
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<div><section>O1<p>15</p></section><section>O2</section><section>O3</section></div>"
);
}
fn make_route_protocol() -> WebUIProtocol {
use webui_protocol::WebUiFragmentRoute;
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<h1>Shell</h1>"),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/".into(),
fragment_id: "dash-page".into(),
exact: true,
keep_alive: false,
..Default::default()
}),
WebUIFragment::route_from(WebUiFragmentRoute {
path: "/contacts/:id".into(),
fragment_id: "detail-page".into(),
exact: true,
keep_alive: true,
..Default::default()
}),
],
contains_boundary: false,
},
);
fragments.insert(
"dash-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Dashboard</p>")],
contains_boundary: false,
},
);
fragments.insert(
"detail-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Detail</p>")],
contains_boundary: false,
},
);
WebUIProtocol::new(fragments)
}
fn make_nested_route_protocol() -> WebUIProtocol {
use webui_protocol::WebUiFragmentRoute;
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/".into(),
fragment_id: "app-shell".into(),
exact: false,
children: vec![WebUiFragmentRoute {
path: "sections/:id".into(),
fragment_id: "section-comp".into(),
exact: false,
children: vec![WebUiFragmentRoute {
path: "topics/:topicId".into(),
fragment_id: "topic-comp".into(),
exact: true,
children: vec![],
keep_alive: false,
..Default::default()
}],
keep_alive: false,
..Default::default()
}],
keep_alive: false,
..Default::default()
})],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<h1>Shell</h1>"),
WebUIFragment::outlet(),
],
contains_boundary: false,
},
);
fragments.insert(
"section-comp".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<h2>Section</h2>"),
WebUIFragment::outlet(),
],
contains_boundary: false,
},
);
fragments.insert(
"topic-comp".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Topic content</p>")],
contains_boundary: false,
},
);
WebUIProtocol::new(fragments)
}
#[test]
fn test_route_renders_shell_always() {
let protocol = make_route_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(html.contains("<h1>Shell</h1>"), "shell should render");
assert!(html.contains(" active>"), "matched route should be active");
assert!(
html.contains("style=\"display:none\""),
"non-matched routes should be hidden"
);
}
#[test]
fn test_route_matched_renders_visible() {
let protocol = make_route_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains("<webui-route path=\"/\""),
"dashboard route should exist"
);
assert!(
html.contains("active>") && html.contains("<dash-page"),
"matched route should be active with component tag: {html}"
);
assert!(
html.contains("<p>Dashboard</p>"),
"matched route should have content"
);
}
#[test]
fn test_route_non_matched_renders_hidden_empty() {
let protocol = make_route_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains("<webui-route path=\"/contacts/:id\""),
"detail route element should exist"
);
let detail_start = html.find("path=\"/contacts/:id\"").expect("detail route");
let after_detail = &html[detail_start..];
assert!(
after_detail.contains("style=\"display:none\">")
&& !after_detail.starts_with(&format!("path=\"/contacts/:id\"{}detail-page>", "")),
"non-matched route should be hidden: {after_detail}"
);
let detail_end = after_detail.find("</webui-route>").expect("closing tag");
let detail_body = &after_detail[..detail_end];
assert!(
!detail_body.contains("<detail-page"),
"non-matched route should not render component content: {detail_body}"
);
}
#[test]
fn test_route_parameterized_match() {
let protocol = make_route_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/contacts/42"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains("active>") && html.contains("<detail-page"),
"detail route should be active: {html}"
);
assert!(html.contains("<p>Detail</p>"), "detail should have content");
let dash_start = html
.find("component=\"dash-page\"")
.expect("dashboard route");
let after_dash = &html[dash_start..];
assert!(
after_dash.contains("style=\"display:none\">"),
"dashboard should be hidden when detail matches: {after_dash}"
);
let dash_end = after_dash.find("</webui-route>").expect("closing tag");
let dash_body = &after_dash[..dash_end];
assert!(
!dash_body.contains("<dash-page"),
"dashboard should not render component content: {dash_body}"
);
}
#[test]
fn test_route_no_match_all_hidden_empty() {
let protocol = make_route_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/nonexistent"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(html.contains("<h1>Shell</h1>"));
assert!(
!html.contains("<p>Dashboard</p>"),
"no route content when nothing matches"
);
assert!(
!html.contains("<p>Detail</p>"),
"no route content when nothing matches"
);
}
#[test]
fn test_route_component_attr_emitted() {
let protocol = make_route_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains("component=\"dash-page\""),
"component attr should be on webui-route: {html}"
);
assert!(
html.contains("component=\"detail-page\""),
"component attr should be on webui-route: {html}"
);
assert!(
html.contains(r#"component="detail-page" exact keep-alive style="display:none">"#),
"keep-alive should be emitted on an unmatched destination placeholder: {html}"
);
}
#[test]
fn test_no_plugin_no_state_attributes() {
let protocol = make_route_protocol();
let state = test_json!({
"title": "Fish & Chips",
"cartOpen": true,
"items": [{"name": "A&B"}]
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
!html.contains("data-state"),
"no data-state without plugin: {html}"
);
assert!(
!html.contains(r#"title="Fish"#),
"no scalar attrs without plugin: {html}"
);
}
#[test]
fn test_nested_routes_render_webui_route_as_light_dom() {
let protocol = make_nested_route_protocol();
let state = test_json!({"title": "Test"});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/sections/frontend"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
assert!(
html.contains("component=\"app-shell\"") && html.contains("active>"),
"root route should be active: {html}"
);
assert!(
!html.contains("<template shadowrootmode"),
"webui-route should be light DOM (no shadow template): {html}"
);
}
#[test]
fn test_nested_routes_render_outlet_as_light_dom() {
let protocol = make_nested_route_protocol();
let state = test_json!({"title": "Test"});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/sections/frontend"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
assert!(
!html.contains("<webui-outlet>"),
"should not contain webui-outlet wrapper: {html}"
);
assert!(
html.contains("<webui-route"),
"should contain webui-route elements: {html}"
);
}
#[test]
fn test_nested_routes_match_child_at_outlet() {
let protocol = make_nested_route_protocol();
let state = test_json!({"title": "Test"});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/sections/frontend"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
assert!(
html.contains("component=\"section-comp\"") && html.contains("active>"),
"section route should be active: {html}"
);
assert!(
html.contains("<h2>Section</h2>"),
"section content should be present: {html}"
);
}
#[test]
fn test_nested_routes_three_levels_deep() {
let protocol = make_nested_route_protocol();
let state = test_json!({"title": "Test"});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/sections/frontend/topics/react"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
assert!(
html.contains("component=\"app-shell\"") && html.contains("active>"),
"root active: {html}"
);
assert!(
html.contains("component=\"section-comp\"") && html.contains("active>"),
"section active: {html}"
);
assert!(
html.contains("component=\"topic-comp\"")
&& html.contains("exact")
&& html.contains("active>"),
"topic active: {html}"
);
assert!(
html.contains("<p>Topic content</p>"),
"leaf content present: {html}"
);
}
#[test]
fn test_nested_routes_nonmatched_siblings_hidden() {
let protocol = make_nested_route_protocol();
let state = test_json!({"title": "Test"});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/sections/frontend"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
assert!(
html.contains(r#"component="topic-comp" exact style="display:none">"#),
"topic should be hidden: {html}"
);
}
#[test]
fn test_nested_routes_preserve_sibling_declaration_order() {
let mut protocol = make_nested_route_protocol();
let Some(Fragment::Route(root)) = protocol
.fragments
.get_mut("index.html")
.and_then(|entry| entry.fragments.first_mut())
.and_then(|fragment| fragment.fragment.as_mut())
else {
panic!("nested route fixture must have a root route");
};
root.children.insert(
0,
webui_protocol::WebUiFragmentRoute {
path: String::new(),
fragment_id: "topic-comp".into(),
exact: true,
..Default::default()
},
);
root.children.push(webui_protocol::WebUiFragmentRoute {
path: "*rest".into(),
fragment_id: "topic-comp".into(),
exact: true,
..Default::default()
});
for path in ["/", "/sections/frontend", "/missing"] {
let mut writer = TestWriter::new();
WebUIHandler::new()
.handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", path),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
let first = html.find(r#"<webui-route path="""#).expect("empty route");
let middle = html
.find(r#"<webui-route path="sections/:id""#)
.expect("section route");
let last = html
.find(r#"<webui-route path="*rest""#)
.expect("catch-all route");
assert!(first < middle && middle < last, "{path}: {html}");
}
}
#[test]
fn test_nested_routes_root_only() {
let protocol = make_nested_route_protocol();
let state = test_json!({"title": "Test"});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
assert!(
html.contains("component=\"app-shell\"") && html.contains("active>"),
"root active at /: {html}"
);
assert!(
html.contains("<h1>Shell</h1>"),
"shell renders at /: {html}"
);
assert!(
html.contains(r#"component="section-comp" style="display:none">"#),
"section hidden at /: {html}"
);
}
#[test]
fn test_css_module_emitted_once_inline_in_component() {
let template = r#"<p><slot></slot></p>"#;
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><div>".to_string()),
WebUIFragment::component("my-card"),
WebUIFragment::raw("A".to_string()),
WebUIFragment::component("my-card"),
WebUIFragment::raw("B</div>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(template.to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
protocol
.components
.entry("my-card".to_string())
.or_default()
.css = "p{color:red}".to_string();
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let count = html.matches(r#"<script type="importmap""#).count();
assert_eq!(
count, 1,
"CSS module importmap should be emitted once, got {count} in: {html}"
);
assert!(
html.contains(r#""my-card":"data:text/css,"#),
"Importmap must register my-card under a data: URI: {html}"
);
let tmpl_count = html.matches(r#"<p><slot></slot></p>"#).count();
assert_eq!(
tmpl_count, 2,
"Template should render twice, got {tmpl_count} in: {html}"
);
let css_pos = html
.find(r#"<script type="importmap""#)
.expect("CSS module importmap missing");
let body_pos = html.find("<body>").expect("<body> missing");
assert!(
css_pos > body_pos,
"CSS module should be inline in component, not in <head>: {html}"
);
}
#[test]
fn test_component_without_css_renders_normally() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("my-card")],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(r#"<p>hello</p>"#.to_string())],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains("<p>hello</p>"),
"Non-module component should render normally: {html}"
);
}
#[test]
fn test_non_module_strategy_no_css_in_head() {
let template = r#"<p>hello</p>"#;
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>".to_string()),
WebUIFragment::component("my-card"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(template.to_string())],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
!html.contains(r#"<style type="module""#),
"Non-module strategy should not emit CSS module tags in <head>: {html}"
);
assert!(
!html.contains(r#"<script type="importmap""#),
"Non-module strategy should not emit CSS module importmaps in <head>: {html}"
);
assert!(
html.contains("<p>hello</p>"),
"Component should still render: {html}"
);
}
#[test]
fn test_style_strategy_embeds_inline_style_in_shadow_template() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><my-card>".to_string()),
WebUIFragment::component("my-card"),
WebUIFragment::raw("</my-card></body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(
"<template shadowrootmode=\"open\"><style>.card{color:red}</style><div>card</div></template>"
.to_string(),
)],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains("<style>.card{color:red}</style>"),
"Style strategy should embed inline CSS in shadow template: {html}"
);
assert!(
!html.contains(r#"<style type="module""#),
"Style strategy should not emit legacy module CSS in <head>: {html}"
);
assert!(
!html.contains(r#"<script type="importmap""#),
"Style strategy should not emit CSS module importmaps in <head>: {html}"
);
}
#[test]
fn test_link_strategy_light_dom_emits_stylesheet_in_head() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><my-card>".to_string()),
WebUIFragment::component("my-card"),
WebUIFragment::raw("</my-card>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>card</div>".to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
let comp = protocol
.components
.entry("my-card".to_string())
.or_default();
comp.css_href = "my-card.css".to_string();
comp.template_json = r#"{"h":"<div>card</div>"}"#.to_string();
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let head_end = html.find("</head>").expect("</head> missing");
let link_pos = html.find(r#"<link rel="stylesheet" href="my-card.css""#);
assert!(
link_pos.is_some_and(|p| p < head_end),
"Light DOM Link strategy should emit <link rel=stylesheet> in <head>: {html}"
);
assert!(
!html.contains(r#"<link rel="preload""#),
"Light DOM Link strategy should NOT emit preload (stylesheet already fetches): {html}"
);
assert!(
!html.contains(r#"<style type="module""#),
"Link strategy should not emit legacy module CSS: {html}"
);
assert!(
!html.contains(r#"<script type="importmap""#),
"Link strategy should not emit CSS module importmaps: {html}"
);
}
#[test]
fn link_strategy_emits_stylesheets_in_deterministic_document_order() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw(
"</head><body><comp-a></comp-a><comp-b></comp-b><comp-c></comp-c>"
.to_string(),
),
WebUIFragment::component("comp-a"),
WebUIFragment::component("comp-b"),
WebUIFragment::component("comp-c"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
for tag in ["comp-a", "comp-b", "comp-c"] {
fragments.insert(
tag.to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(format!("<div>{tag}</div>"))],
contains_boundary: false,
},
);
}
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
for tag in ["comp-a", "comp-b", "comp-c"] {
let comp = protocol.components.entry(tag.to_string()).or_default();
comp.css_href = format!("{tag}.css");
comp.template_json = format!(r#"{{"h":"<div>{tag}</div>"}}"#);
}
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let pos_a = html
.find(r#"href="comp-a.css""#)
.expect("comp-a.css link missing");
let pos_b = html
.find(r#"href="comp-b.css""#)
.expect("comp-b.css link missing");
let pos_c = html
.find(r#"href="comp-c.css""#)
.expect("comp-c.css link missing");
assert!(
pos_a < pos_b && pos_b < pos_c,
"stylesheet <link> tags must follow document/traversal order \
(comp-a, comp-b, comp-c), got positions {pos_a}, {pos_b}, {pos_c}: {html}"
);
}
#[test]
fn link_strategy_preloads_static_shadow_css_in_head() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><my-card>".to_string()),
WebUIFragment::component("my-card"),
WebUIFragment::raw("</my-card></body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"o-loading-state".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>loading</div>".to_string())],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<template shadowrootmode=\"open\">".to_string()),
structural_fragment("shadow_styles:my-card"),
WebUIFragment::raw("<div>card</div></template>".to_string()),
],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
let component = protocol
.components
.entry("my-card".to_string())
.or_default();
component.css_href = "my-card.css".to_string();
component.template_json = r#"{"h":"<div>card</div>"}"#.to_string();
component.uses_shadow_dom = true;
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let head_end = html.find("</head>").expect("</head> missing");
let preload = html
.find(r#"<link rel="preload" as="style" href="my-card.css">"#)
.expect("Shadow preload missing");
let stylesheet = html
.find(r#"<link rel="stylesheet" href="my-card.css""#)
.expect("Shadow stylesheet missing");
assert!(preload < head_end);
assert!(stylesheet > head_end);
assert_eq!(html.matches(r#"rel="preload" as="style""#).count(), 1);
}
#[test]
fn tree_local_styles_deduplicate_per_root_and_resume_caller_frame() {
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><outer-box>".to_string()),
WebUIFragment::component("outer-box"),
WebUIFragment::raw("</outer-box></body></html>".to_string()),
],
contains_boundary: false,
},
),
(
"outer-box".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<template shadowrootmode=\"open\">".to_string()),
structural_fragment("shadow_styles:outer-box"),
WebUIFragment::raw("<light-card>".to_string()),
WebUIFragment::component("light-card"),
WebUIFragment::raw("</light-card><light-card>".to_string()),
WebUIFragment::component("light-card"),
WebUIFragment::raw("</light-card><inner-box>".to_string()),
WebUIFragment::component("inner-box"),
WebUIFragment::raw("</inner-box><light-card>".to_string()),
WebUIFragment::component("light-card"),
WebUIFragment::raw("</light-card></template>".to_string()),
],
contains_boundary: false,
},
),
(
"inner-box".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<template shadowrootmode=\"open\">".to_string()),
structural_fragment("shadow_styles:inner-box"),
WebUIFragment::raw("<light-card>".to_string()),
WebUIFragment::component("light-card"),
WebUIFragment::raw("</light-card></template>".to_string()),
],
contains_boundary: false,
},
),
(
"light-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>card</p>".to_string())],
contains_boundary: false,
},
),
]);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Style);
for (tag, css, uses_shadow_dom) in [
("outer-box", ".outer{}", true),
("inner-box", ".inner{}", true),
("light-card", ".card{}", false),
] {
let component = protocol.components.entry(tag.to_string()).or_default();
component.css = css.to_string();
component.uses_shadow_dom = uses_shadow_dom;
}
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/").with_nonce("css-nonce"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert_eq!(html.matches("data-webui-resource=\"outer-box\"").count(), 1);
assert_eq!(html.matches("data-webui-resource=\"inner-box\"").count(), 1);
assert_eq!(
html.matches("data-webui-resource=\"light-card\"").count(),
2,
"the Light resource installs once in each Shadow tree: {html}"
);
assert_eq!(
html.matches("data-webui-resource=").count(),
4,
"outer-box + light-card and inner-box + light-card are the four tree-local installs"
);
assert_eq!(
html.matches("nonce=\"css-nonce\"").count(),
4,
"each inline style resource carries the nonce"
);
assert!(html.contains(r#"<meta name="webui-nonce" content="css-nonce">"#));
let inner_end = html.find("</inner-box>").expect("inner host close");
let caller_continuation = &html[inner_end..];
let trailing_card = caller_continuation
.find("<p>card</p>")
.expect("caller continues after nested Shadow root");
assert!(trailing_card > 0);
assert!(
!caller_continuation.contains("data-webui-resource=\"light-card\""),
"returning from the nested Shadow root must restore the outer frame's delivered set"
);
}
#[test]
fn routed_document_styles_are_hoisted_for_the_active_chain() {
let route = WebUiFragmentRoute {
path: "/".to_string(),
fragment_id: "app-shell".to_string(),
children: vec![
WebUiFragmentRoute {
path: String::new(),
fragment_id: "dashboard-page".to_string(),
exact: true,
..Default::default()
},
WebUiFragmentRoute {
path: "details".to_string(),
fragment_id: "detail-page".to_string(),
exact: true,
..Default::default()
},
],
..Default::default()
};
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::boundary(0, "index.html", "boundary-0", None),
WebUIFragment::route_from(route),
WebUIFragment::boundary_end(0),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: true,
},
),
(
"app-shell".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<header>App</header>"),
WebUIFragment::outlet(),
],
contains_boundary: false,
},
),
(
"dashboard-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>Dashboard</main>")],
contains_boundary: false,
},
),
(
"detail-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>Detail</main>")],
contains_boundary: false,
},
),
]);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
for tag in ["app-shell", "dashboard-page", "detail-page"] {
let component = protocol.components.entry(tag.to_string()).or_default();
component.css_href = format!("/{tag}.css");
component.template_json = r#"{"h":""}"#.to_string();
}
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert_eq!(
html.matches(r#"data-webui-resource="app-shell""#).count(),
1,
"the matched route shell must install once: {html}"
);
assert_eq!(
html.matches(r#"data-webui-resource="dashboard-page""#)
.count(),
1,
"the matched child route must install once: {html}"
);
assert!(
!html.contains(r#"data-webui-resource="detail-page""#),
"inactive route CSS must not be delivered: {html}"
);
let head_end = html.find("</head>").expect("head close");
let head = &html[..head_end];
for (resource, href) in [
("app-shell", "/app-shell.css"),
("dashboard-page", "/dashboard-page.css"),
] {
let encoded_href = crate::html_encode::encode_safe(href);
assert!(
head.contains(&format!(
r#"<link rel="stylesheet" href="{encoded_href}" data-webui-resource="{resource}" data-webui-strategy="link">"#
)),
"Document-targeted route CSS must be render-blocking in head: {html}"
);
assert!(
!head.contains(&format!(
r#"<link rel="preload" as="style" href="{encoded_href}">"#
)),
"a hoisted stylesheet must not also be preloaded: {html}"
);
}
assert!(
!head.contains("detail-page.css"),
"inactive route CSS must not be preloaded: {html}"
);
let app_style = html
.find(r#"data-webui-resource="app-shell""#)
.expect("app style");
let dashboard_style = html
.find(r#"data-webui-resource="dashboard-page""#)
.expect("dashboard style");
let app_host = html.find("<app-shell").expect("app route host");
let dashboard_host = html.find("<dashboard-page").expect("dashboard route host");
assert!(
app_style < dashboard_style
&& dashboard_style < head_end
&& head_end < app_host
&& app_host < dashboard_host,
"matched Document route closures must be hoisted in chain order: {html}"
);
let protocol = Protocol::new(protocol);
let mut streamed = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut streamed,
)
.unwrap();
let streamed_head_end = streamed
.output
.find("</head>")
.expect("streamed head close");
for resource in ["app-shell", "dashboard-page"] {
let marker = format!(r#"data-webui-resource="{resource}""#);
let position = streamed.output.find(&marker).expect("streamed route style");
assert!(
position < streamed_head_end,
"streaming must hoist {resource} into head: {}",
streamed.output
);
assert_eq!(
streamed.output.matches(&marker).count(),
1,
"streaming must not emit {resource} twice: {}",
streamed.output
);
}
}
#[test]
fn bundled_routed_document_stylesheet_is_hoisted_once() {
let route = WebUiFragmentRoute {
path: "/".to_string(),
fragment_id: "dashboard-page".to_string(),
exact: true,
..Default::default()
};
let mut protocol = WebUIProtocol::new(HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
WebUIFragment::route_from(route),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
),
(
"dashboard-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>Dashboard</main>")],
contains_boundary: false,
},
),
]));
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
for tag in ["dashboard-page", "summary-card", "activity-list"] {
protocol
.components
.entry(tag.to_string())
.or_default()
.css_href = format!("/{tag}.css");
}
protocol.style_closures.insert(
"index.html".to_string(),
webui_protocol::ComponentStyleClosure::default(),
);
protocol.style_closures.insert(
"dashboard-page".to_string(),
webui_protocol::ComponentStyleClosure {
component_tags: vec![
"dashboard-page".to_string(),
"summary-card".to_string(),
"activity-list".to_string(),
],
style_chunks: vec![0],
},
);
protocol.style_chunks.push(webui_protocol::StyleChunk {
name: "_chunk-dashboard-page-3".to_string(),
css: String::new(),
css_href: "/_chunk-dashboard-page-3.css".to_string(),
component_tags: vec![
"dashboard-page".to_string(),
"summary-card".to_string(),
"activity-list".to_string(),
],
});
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let head_end = html.find("</head>").expect("head close");
let marker = r#"data-webui-resource="_chunk-dashboard-page-3""#;
let style = html.find(marker).expect("bundled route stylesheet");
assert!(
style < head_end,
"bundled route CSS must be in head: {html}"
);
assert_eq!(
html.matches(marker).count(),
1,
"the route host must not emit the hoisted chunk again: {html}"
);
let bundled_href = crate::html_encode::encode_safe("/_chunk-dashboard-page-3.css");
assert!(
!html.contains(&format!(
r#"<link rel="preload" as="style" href="{bundled_href}">"#
)),
"the applied bundled stylesheet must not also be preloaded: {html}"
);
}
#[test]
fn routed_document_inline_styles_are_hoisted() {
for strategy in [
webui_protocol::CssStrategy::Style,
webui_protocol::CssStrategy::Module,
] {
let route = WebUiFragmentRoute {
path: "/".to_string(),
fragment_id: "dashboard-page".to_string(),
exact: true,
..Default::default()
};
let mut protocol = WebUIProtocol::new(HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
WebUIFragment::route_from(route),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
),
(
"dashboard-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>Dashboard</main>")],
contains_boundary: false,
},
),
]));
protocol.set_css_strategy(strategy);
protocol
.components
.entry("dashboard-page".to_string())
.or_default()
.css = ".dashboard{display:grid}".to_string();
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/").with_nonce("css-nonce"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let head_end = html.find("</head>").expect("head close");
let marker = r#"data-webui-resource="dashboard-page""#;
let style = html.find(marker).expect("route style fallback");
let host = html.find("<dashboard-page").expect("dashboard route host");
let strategy_name = if strategy == webui_protocol::CssStrategy::Module {
"module"
} else {
"style"
};
let style_marker = format!(
r#"data-webui-resource="dashboard-page" data-webui-strategy="{strategy_name}""#
);
assert!(
style < head_end && head_end < host,
"{strategy:?} route CSS must be applied in head before its host: {html}"
);
assert_eq!(
html.matches(&style_marker).count(),
1,
"{strategy:?} route CSS must not be emitted twice: {html}"
);
}
}
#[test]
fn light_route_inside_static_shadow_root_is_only_preloaded_in_head() {
let route = WebUiFragmentRoute {
path: "/".to_string(),
fragment_id: "dashboard-page".to_string(),
exact: true,
..Default::default()
};
let mut protocol = WebUIProtocol::new(HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><outer-box>"),
WebUIFragment::component("outer-box"),
WebUIFragment::raw("</outer-box></body></html>"),
],
contains_boundary: false,
},
),
(
"outer-box".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<template shadowrootmode=\"open\">"),
structural_fragment("shadow_styles:outer-box"),
WebUIFragment::route_from(route),
WebUIFragment::raw("</template>"),
],
contains_boundary: false,
},
),
(
"dashboard-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>Dashboard</main>")],
contains_boundary: false,
},
),
]));
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
for tag in ["outer-box", "dashboard-page"] {
protocol
.components
.entry(tag.to_string())
.or_default()
.css_href = format!("/{tag}.css");
}
protocol
.components
.get_mut("outer-box")
.expect("outer-box component")
.uses_shadow_dom = true;
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let head_end = html.find("</head>").expect("head close");
let head = &html[..head_end];
let dashboard_style = html
.find(r#"data-webui-resource="dashboard-page""#)
.expect("tree-local dashboard stylesheet");
let shadow_start = html
.find("<template shadowrootmode=\"open\">")
.expect("Shadow template");
let dashboard_host = html.find("<dashboard-page").expect("dashboard route host");
let dashboard_href = crate::html_encode::encode_safe("/dashboard-page.css");
assert!(
head.contains(&format!(
r#"<link rel="preload" as="style" href="{dashboard_href}">"#
)),
"Shadow-targeted route CSS must start loading from head: {html}"
);
assert!(
!head.contains(r#"data-webui-resource="dashboard-page""#),
"Shadow-targeted route CSS must not be applied to Document: {html}"
);
assert!(
shadow_start < dashboard_style && dashboard_style < dashboard_host,
"the applying stylesheet must remain inside the owning ShadowRoot: {html}"
);
}
#[test]
fn isolated_shadow_entry_does_not_emit_document_preloads() {
let route = WebUiFragmentRoute {
path: "/".to_string(),
fragment_id: "dashboard-page".to_string(),
exact: true,
..Default::default()
};
let mut protocol = WebUIProtocol::new(HashMap::from([
(
"outer-box".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<template shadowrootmode=\"open\">"),
structural_fragment("shadow_styles:outer-box"),
WebUIFragment::route_from(route),
WebUIFragment::raw("</template>"),
],
contains_boundary: false,
},
),
(
"dashboard-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>Dashboard</main>")],
contains_boundary: false,
},
),
]));
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
for tag in ["outer-box", "dashboard-page"] {
protocol
.components
.entry(tag.to_string())
.or_default()
.css_href = format!("/{tag}.css");
}
protocol
.components
.get_mut("outer-box")
.expect("outer-box component")
.uses_shadow_dom = true;
protocol.populate_style_closures(&["outer-box"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("outer-box", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.starts_with("<template shadowrootmode=\"open\">"),
"isolated component output must not be prefixed with document metadata: {html}"
);
assert!(
!html.contains(r#"<link rel="preload""#),
"an isolated Shadow entry has no Document head for preloads: {html}"
);
assert!(
html.contains(r#"data-webui-resource="dashboard-page""#),
"the route stylesheet must still install inside the ShadowRoot: {html}"
);
}
#[test]
fn routed_shadow_styles_stay_tree_local_and_deduplicate_static_resources() {
let route = WebUiFragmentRoute {
path: "/".to_string(),
fragment_id: "app-shell".to_string(),
children: vec![
WebUiFragmentRoute {
path: String::new(),
fragment_id: "dashboard-page".to_string(),
exact: true,
..Default::default()
},
WebUiFragmentRoute {
path: "details".to_string(),
fragment_id: "detail-page".to_string(),
exact: true,
..Default::default()
},
],
..Default::default()
};
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::boundary(0, "index.html", "boundary-0", None),
WebUIFragment::route_from(route),
WebUIFragment::boundary_end(0),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: true,
},
),
(
"app-shell".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<template shadowrootmode=\"open\">"),
structural_fragment("shadow_styles:app-shell"),
WebUIFragment::raw("<shared-card"),
structural_fragment("streaming_root:shared-card"),
WebUIFragment::raw(">"),
WebUIFragment::component("shared-card"),
WebUIFragment::raw("</shared-card>"),
WebUIFragment::outlet(),
WebUIFragment::raw("</template>"),
],
contains_boundary: false,
},
),
(
"dashboard-page".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<shared-card"),
structural_fragment("streaming_root:shared-card"),
WebUIFragment::raw(">"),
WebUIFragment::component("shared-card"),
WebUIFragment::raw("</shared-card><main>Dashboard</main>"),
],
contains_boundary: false,
},
),
(
"detail-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<main>Detail</main>")],
contains_boundary: false,
},
),
(
"shared-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Shared</p>")],
contains_boundary: false,
},
),
]);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Style);
for tag in ["app-shell", "dashboard-page", "detail-page", "shared-card"] {
let component = protocol.components.entry(tag.to_string()).or_default();
component.css = format!(".{tag}{{display:block}}");
component.template_json = r#"{"h":""}"#.to_string();
}
protocol
.components
.get_mut("app-shell")
.expect("app shell component")
.uses_shadow_dom = true;
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
for resource in ["app-shell", "shared-card", "dashboard-page"] {
assert_eq!(
html.matches(&format!(r#"data-webui-resource="{resource}""#))
.count(),
1,
"{resource} must install once in the active ShadowRoot: {html}"
);
}
assert!(
!html.contains(r#"data-webui-resource="detail-page""#),
"inactive route CSS must not enter the ShadowRoot: {html}"
);
let shadow_start = html
.find("<template shadowrootmode=\"open\">")
.expect("Shadow template");
let dashboard_style = html
.find(r#"data-webui-resource="dashboard-page""#)
.expect("dashboard style");
let dashboard_host = html.find("<dashboard-page").expect("dashboard route host");
let shadow_end = html.find("</template>").expect("Shadow template close");
assert!(
shadow_start < dashboard_style
&& dashboard_style < dashboard_host
&& dashboard_host < shadow_end,
"active route CSS must precede its generated host inside the owning ShadowRoot: {html}"
);
let protocol = Protocol::new(protocol);
let mut streamed = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut streamed,
)
.unwrap();
for resource in ["app-shell", "shared-card"] {
assert_eq!(
streamed
.output
.matches(&format!(r#"data-webui-resource="{resource}""#))
.count(),
1,
"streaming must preserve tree-local delivery for {resource}: {}",
streamed.output
);
}
assert!(
!streamed
.output
.contains(r#"data-webui-resource="dashboard-page""#),
"route CSS is carried by the streamed checkpoint rather than installed before the route host: {}",
streamed.output
);
assert!(
streamed
.output
.contains(r#""dashboard-page":{"css":".dashboard-page{display:block}""#)
&& streamed
.output
.contains(r#""dashboard-page":["dashboard-page","shared-card"]"#),
"streamed route CSS metadata must preserve tree ownership and cascade order: {}",
streamed.output
);
assert!(
!streamed
.output
.contains(r#"data-webui-resource="detail-page""#),
"streaming must omit inactive route CSS: {}",
streamed.output
);
}
#[test]
fn document_styles_follow_doctype_before_headless_document_content() {
for entry_fragments in [
vec![
WebUIFragment::raw("<!DOCTYPE html><html><body>"),
structural_fragment("body_start"),
WebUIFragment::raw("<my-card>"),
WebUIFragment::component("my-card"),
WebUIFragment::raw("</my-card></body></html>"),
],
vec![
WebUIFragment::raw("<!doctype html><html><my-card>"),
WebUIFragment::component("my-card"),
WebUIFragment::raw("</my-card></html>"),
],
] {
let mut protocol = WebUIProtocol::new(HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: entry_fragments,
contains_boundary: false,
},
),
(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>card</p>")],
contains_boundary: false,
},
),
]));
protocol.set_css_strategy(webui_protocol::CssStrategy::Style);
let component = protocol
.components
.entry("my-card".to_string())
.or_default();
component.css = ".card{color:red}".to_string();
component.uses_shadow_dom = false;
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.expect("render");
let html = writer.get_content();
assert!(
html.to_ascii_lowercase().starts_with("<!doctype html>"),
"the doctype must remain the first document token: {html}"
);
let style = html.find("data-webui-resource=\"my-card\"").expect("style");
let document = html.find("<html").expect("document root");
let component = html.find("<my-card>").expect("component host");
assert!(
style < document && style < component,
"headless document styles must precede document content: {html}"
);
}
}
#[test]
fn authored_shadow_component_can_render_as_entry_fragment() {
let mut protocol = WebUIProtocol::new(HashMap::from([(
"my-card".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<template shadowrootmode=\"open\">"),
structural_fragment("shadow_styles:my-card"),
WebUIFragment::raw("<p>card</p></template>"),
],
contains_boundary: false,
},
)]));
protocol.set_css_strategy(webui_protocol::CssStrategy::Style);
let component = protocol
.components
.entry("my-card".to_string())
.or_default();
component.css = ".card{color:red}".to_string();
component.uses_shadow_dom = true;
protocol.populate_style_closures(&["my-card"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("my-card", "/"),
&mut writer,
)
.expect("render");
let html = writer.get_content();
assert!(html.starts_with("<template shadowrootmode=\"open\"><style "));
assert_eq!(html.matches("data-webui-resource=\"my-card\"").count(), 1);
assert!(html.ends_with("<p>card</p></template>"));
}
#[test]
fn component_style_closures_escape_mixed_case_style_end_tags() {
for strategy in [
webui_protocol::CssStrategy::Style,
webui_protocol::CssStrategy::Module,
] {
let mut protocol = WebUIProtocol::new(HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><safe-card>"),
WebUIFragment::component("safe-card"),
WebUIFragment::raw("</safe-card></body></html>"),
],
contains_boundary: false,
},
),
(
"safe-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Safe</p>")],
contains_boundary: false,
},
),
]));
protocol.set_css_strategy(strategy);
protocol.components.insert(
"safe-card".to_string(),
webui_protocol::ComponentData {
css: ".safe{content:'</StYlE>'}".to_string(),
..Default::default()
},
);
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(html.contains(".safe{content:'<\\/StYlE>'}"), "{html}");
assert!(!html.contains(".safe{content:'</StYlE>'}"), "{html}");
}
}
#[test]
fn shadow_closure_supports_link_and_module_ssr_paths() {
for strategy in [
webui_protocol::CssStrategy::Link,
webui_protocol::CssStrategy::Module,
] {
let mut protocol = WebUIProtocol::new(HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><my-card>".to_string()),
WebUIFragment::component("my-card"),
WebUIFragment::raw("</my-card></body></html>".to_string()),
],
contains_boundary: false,
},
),
(
"my-card".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<template shadowrootmode=\"open\">".to_string()),
structural_fragment("shadow_styles:my-card"),
WebUIFragment::raw("<p>card</p></template>".to_string()),
],
contains_boundary: false,
},
),
]));
protocol.set_css_strategy(strategy);
let component = protocol
.components
.entry("my-card".to_string())
.or_default();
component.css = ".card{color:red}".to_string();
component.css_href = "/my-card.css".to_string();
component.uses_shadow_dom = true;
protocol.populate_style_closures(&["index.html"]);
let mut writer = TestWriter::new();
handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
match strategy {
webui_protocol::CssStrategy::Link => {
let escaped_href = crate::html_encode::encode_safe("/my-card.css");
assert!(html.contains(&format!(
"<template shadowrootmode=\"open\"><link rel=\"stylesheet\" href=\"{escaped_href}\" data-webui-resource=\"my-card\" data-webui-strategy=\"link\">"
)));
}
webui_protocol::CssStrategy::Module => {
assert!(html.contains(
"<template shadowrootmode=\"open\"><style data-webui-resource=\"my-card\" data-webui-strategy=\"module\">.card{color:red}</style>"
));
assert_eq!(html.matches("<script type=\"importmap\"").count(), 1);
}
webui_protocol::CssStrategy::Style => unreachable!(),
}
}
}
#[test]
fn test_module_preloads_emit_in_head_in_compiler_order() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw(
r#"<html><head><script type="module" async src="/index.js"></script>"#
.to_string(),
),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.module_preloads = vec!["/chunk-big.js".to_string(), "/chunk-small.js".to_string()];
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let head_end = html.find("</head>").expect("</head> missing");
let head = &html[..head_end];
assert!(
head.contains(
r#"<link rel="modulepreload" href="/chunk-big.js"><link rel="modulepreload" href="/chunk-small.js">"#
),
"hints must appear in <head> in the compiler's order: {html}"
);
}
#[test]
fn test_no_module_preloads_emits_nothing() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert!(
!writer.get_content().contains("modulepreload"),
"a build without hints must be byte-identical to before"
);
}
#[test]
fn test_link_strategy_head_links_follow_document_order() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><z-widget>".to_string()),
WebUIFragment::component("z-widget"),
WebUIFragment::raw("</z-widget><a-widget>".to_string()),
WebUIFragment::component("a-widget"),
WebUIFragment::raw("</a-widget>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"z-widget".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>z</div>".to_string())],
contains_boundary: false,
},
);
fragments.insert(
"a-widget".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>a</div>".to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
let z = protocol
.components
.entry("z-widget".to_string())
.or_default();
z.css_href = "z-widget.css".to_string();
z.template_json = r#"{"h":"<div>z</div>"}"#.to_string();
let a = protocol
.components
.entry("a-widget".to_string())
.or_default();
a.css_href = "a-widget.css".to_string();
a.template_json = r#"{"h":"<div>a</div>"}"#.to_string();
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let head_end = html.find("</head>").expect("</head> missing");
let head_section = &html[..head_end];
let z_pos = head_section
.find(r#"<link rel="stylesheet" href="z-widget.css""#)
.expect("z-widget stylesheet link missing from <head>");
let a_pos = head_section
.find(r#"<link rel="stylesheet" href="a-widget.css""#)
.expect("a-widget stylesheet link missing from <head>");
assert!(
z_pos < a_pos,
"CSS <link> tags must follow document order (z-widget before \
a-widget), not alphabetical order: {html}"
);
}
#[test]
fn test_css_module_emitted_in_component_light_dom() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><my-card>".to_string()),
WebUIFragment::component("my-card"),
WebUIFragment::raw("</my-card>".to_string()),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"my-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(r#"<p>hi</p>"#.to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
protocol
.components
.entry("my-card".to_string())
.or_default()
.css = "p{color:red}".to_string();
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let tag_open = html.find("<my-card>").expect("<my-card> missing");
let css_pos = html
.find(r#"<script type="importmap""#)
.expect("CSS module importmap missing");
let tag_close = html.rfind("</my-card>").expect("</my-card> missing");
assert!(
css_pos > tag_open && css_pos < tag_close,
"CSS module should be inside component light DOM: {html}"
);
let head_end = html.find("</head>").expect("</head> missing");
assert!(
css_pos > head_end,
"CSS module should not be in <head>: {html}"
);
}
#[test]
fn styled_protocol_without_closures_is_rejected_for_full_ssr() {
let mut protocol = WebUIProtocol::default();
protocol.fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<legacy-card>"),
WebUIFragment::component("legacy-card"),
WebUIFragment::raw("</legacy-card>"),
],
contains_boundary: false,
},
),
(
"legacy-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Legacy</p>")],
contains_boundary: false,
},
),
]);
protocol.components.insert(
"legacy-card".to_string(),
webui_protocol::ComponentData {
css: ".legacy{color:red}".to_string(),
..Default::default()
},
);
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
let mut writer = TestWriter::new();
let error = handle(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.expect_err("styled protocols require closure metadata");
assert!(error
.to_string()
.contains("component style closure metadata is required"));
}
#[test]
fn test_css_module_emitted_for_route_components() {
let template = r#"<h1>Dashboard</h1>"#;
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>".to_string()),
WebUIFragment::route("/", "dash-page"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"dash-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(template.to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
let comp = protocol
.components
.entry("dash-page".to_string())
.or_default();
comp.css = "h1{font-size:2rem}".to_string();
comp.template_json = r#"{"h":"<h1>Dashboard</h1>"}"#.to_string();
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains(r#""dash-page":"data:text/css,h1{font-size:2rem}""#),
"Route component should have CSS module importmap with data: URI: {html}"
);
assert!(
html.contains("<h1>Dashboard</h1>"),
"Route component should render content: {html}"
);
let fallback = html
.find(r#"data-webui-resource="dash-page" data-webui-strategy="module""#)
.expect("route Module fallback");
let host_open = html.find("<dash-page>").expect("route host");
let content = html.find("<h1>Dashboard</h1>").expect("route content");
assert!(
fallback < host_open && host_open < content,
"the Module fallback must precede the generated Light route host: {html}"
);
let importmap = html
.find(r#"<script type="importmap""#)
.expect("route CSS module importmap");
assert!(
host_open < importmap,
"the CSS module importmap must live inside the route host, not as a route child: {html}"
);
}
#[test]
fn test_head_css_link_skipped_for_components_without_css() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>".to_string()),
WebUIFragment::component("has-css"),
WebUIFragment::component("no-css"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"has-css".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>styled</p>".to_string())],
contains_boundary: false,
},
);
fragments.insert(
"no-css".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>plain</p>".to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
protocol
.components
.entry("has-css".to_string())
.or_default()
.css_href = "has-css.css".to_string();
protocol
.components
.entry("no-css".to_string())
.or_default()
.template_json = r#"{"h":"<p>plain</p>"}"#.to_string();
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({});
let mut writer = TestWriter::new();
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains(r#"<link rel="stylesheet" href="has-css.css""#),
"Component with CSS should get a <link rel=stylesheet> in <head>: {html}"
);
assert!(
!html.contains("no-css.css"),
"Component without CSS must NOT get a <link> in <head>: {html}"
);
}
#[test]
fn test_reachable_unrendered_components_get_templates_and_css_but_not_inventory() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><app-shell>".to_string()),
WebUIFragment::component("app-shell"),
WebUIFragment::raw("</app-shell>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<div>Shell</div>".to_string()),
WebUIFragment::component("cart-panel"),
],
contains_boundary: false,
},
);
fragments.insert(
"cart-panel".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<aside>".to_string()),
WebUIFragment::if_cond(ConditionExpr::identifier("hasItems"), "cart-items"),
WebUIFragment::raw("</aside>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"cart-items".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("product-card")],
contains_boundary: false,
},
);
fragments.insert(
"product-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Card</div>".to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
for name in ["app-shell", "cart-panel", "product-card"] {
let comp = protocol.components.entry(name.to_string()).or_default();
comp.template_json = format!(r#"{{"h":"<div class=\"{name}\"></div>"}}"#);
comp.css = format!(".{name}{{display:block}}");
if name == "cart-panel" {
comp.hydration_mode = StateProjectionMode::Keys as i32;
comp.hydration_keys = vec!["hasItems".to_string()];
}
if name == "product-card" {
comp.template_functions = r#"[function(v,s){return !!v("ready",s)}]"#.to_string();
}
}
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({ "hasItems": false });
let mut writer = TestWriter::new();
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains(r#"<script type="application/json" id="webui-data">"#),
"non-executable SSR metadata should be emitted in the webui-data block: {html}"
);
assert!(
html.contains(r#""state":{"hasItems":false}"#),
"SSR state should live in the JSON data block: {html}"
);
assert!(
html.contains(r#""inventory":"#),
"SSR inventory should live in the JSON data block: {html}"
);
assert!(
!html.contains("window.__webui={\""),
"executable bootstrap must not embed the window.__webui JSON literal: {html}"
);
assert!(
!html.contains(r#"document.getElementById("webui-data")"#),
"SSR must not parse webui-data; client packages own that lazy load: {html}"
);
assert!(
!html.contains("window.__webui=w;"),
"executable bootstrap must not replace existing window.__webui registrations: {html}"
);
assert!(
!html.contains("w.templateFns={\""),
"template function emission must not replace existing templateFns registrations: {html}"
);
assert!(
html.contains(r#"var f=w.templateFns||(w.templateFns={});f["product-card"]=[function(v,s){return !!v("ready",s)}];"#),
"template functions should merge into the flat templateFns registry: {html}"
);
assert!(
html.contains(r#""product-card":{"h":"<div class=\"product-card\"><\/div>"}"#),
"product-card template should be emitted even when unrendered: {html}"
);
assert!(
html.contains(r#""product-card":"data:text/css,"#),
"reachable product-card CSS module importmap should be emitted: {html}"
);
assert!(
html.contains(r#""app-shell":{"h":"<div class=\"app-shell\"><\/div>"}"#),
"rendered app-shell template should be emitted: {html}"
);
assert!(
html.contains(r#""cart-panel":{"h":"<div class=\"cart-panel\"><\/div>"}"#),
"rendered cart-panel template should be emitted: {html}"
);
}
#[test]
fn component_asset_manifest_falls_back_to_body_start() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<body>".to_string()),
structural_fragment("body_start"),
WebUIFragment::raw("<main>Body-only host</main></body>".to_string()),
],
contains_boundary: false,
},
)]);
let mut protocol = WebUIProtocol::new(fragments);
protocol.component_asset_style_preloads = vec![ComponentAssetStylePreload {
root: "lazy-panel".to_string(),
style_hrefs: vec!["/assets/lazy-panel.css".to_string()],
}];
let mut writer = TestWriter::new();
handle(
&protocol,
&Value::Null,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
r#"<body><script type="application/json" id="webui-component-assets">{"lazy-panel":["/assets/lazy-panel.css"]}</script><main>Body-only host</main></body>"#
);
}
#[test]
fn component_asset_styles_emit_once_when_body_precedes_head() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<body>".to_string()),
structural_fragment("body_start"),
WebUIFragment::raw("</body><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head>".to_string()),
],
contains_boundary: false,
},
)]);
let mut protocol = WebUIProtocol::new(fragments);
protocol.component_asset_style_preloads = vec![ComponentAssetStylePreload {
root: "lazy-panel".to_string(),
style_hrefs: vec!["/assets/lazy-panel.css".to_string()],
}];
let mut writer = TestWriter::new();
handle(
&protocol,
&Value::Null,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer
.get_content()
.matches(r#"id="webui-component-assets""#)
.count(),
1
);
}
#[test]
fn light_component_asset_styles_apply_at_head_end() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body></body></html>".to_string()),
],
contains_boundary: false,
},
)]);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Link);
for root in ["lazy-panel", "secondary-panel"] {
protocol.components.insert(
root.to_string(),
webui_protocol::ComponentData {
uses_shadow_dom: false,
..Default::default()
},
);
}
protocol.component_asset_style_preloads = vec![
ComponentAssetStylePreload {
root: "lazy-panel".to_string(),
style_hrefs: vec![
"/assets/lazy-panel.css".to_string(),
"/assets/shared.css".to_string(),
],
},
ComponentAssetStylePreload {
root: "secondary-panel".to_string(),
style_hrefs: vec![
"/assets/secondary-panel.css".to_string(),
"/assets/shared.css".to_string(),
],
},
];
let mut writer = TestWriter::new();
handle(
&protocol,
&Value::Null,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert_eq!(html.matches(r#"rel="stylesheet""#).count(), 3);
assert_eq!(html.matches("/assets/shared.css").count(), 1);
assert!(!html.contains(COMPONENT_ASSET_MANIFEST_ID));
let links_end = html
.find(r#"<link rel="stylesheet" href="/assets/secondary-panel.css">"#)
.expect("asset stylesheet must be emitted");
let head_end = html.find("</head>").expect("head must close");
assert!(links_end < head_end);
}
#[test]
fn test_css_module_emits_nonce_attribute_when_nonce_set() {
let template = r#"<h1>Dashboard</h1>"#;
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>".to_string()),
WebUIFragment::route("/", "dash-page"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"dash-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(template.to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
let comp = protocol
.components
.entry("dash-page".to_string())
.or_default();
comp.css = "h1{font-size:2rem}".to_string();
comp.template_json = r#"{"h":"<h1>Dashboard</h1>"}"#.to_string();
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/").with_nonce("test-nonce-123"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains(
r#"<script type="importmap" nonce="test-nonce-123" data-webui-resource="dash-page">{"imports":{"dash-page":"data:text/css,h1{font-size:2rem}"}}</script>"#
),
"CSS module importmap tag should include nonce attribute in canonical order: {html}"
);
}
#[test]
fn test_unrendered_css_module_emits_nonce_attribute_when_nonce_set() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body><app-shell>".to_string()),
WebUIFragment::component("app-shell"),
WebUIFragment::raw("</app-shell>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::identifier("hasItems"),
"cart-items",
)],
contains_boundary: false,
},
);
fragments.insert(
"cart-items".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("product-card")],
contains_boundary: false,
},
);
fragments.insert(
"product-card".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<div>Card</div>".to_string())],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.set_css_strategy(webui_protocol::CssStrategy::Module);
for name in ["app-shell", "product-card"] {
let comp = protocol.components.entry(name.to_string()).or_default();
comp.template_json = format!(r#"{{"h":"<div class=\"{name}\"></div>"}}"#);
comp.css = format!(".{name}{{display:block}}");
}
protocol.populate_style_closures(&["index.html"]);
let state = test_json!({ "hasItems": false });
let mut writer = TestWriter::new();
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/").with_nonce("test-nonce-123"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(
html.contains(
r#"<script type="importmap" nonce="test-nonce-123" data-webui-resource="product-card">{"imports":{"product-card":"data:text/css,.product-card{display:block}"}}</script>"#
),
"Unrendered (body_end) CSS module importmap tag should include nonce attribute in canonical order: {html}"
);
}
#[test]
fn projected_state_excludes_non_hydration_keys() -> Result<()> {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body><style>".to_string()),
WebUIFragment::signal("tokens.light", true),
WebUIFragment::raw("</style>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<span>shell</span>".to_string())],
contains_boundary: false,
},
);
let index_fragments = fragments
.get_mut("index.html")
.expect("index fixture should exist");
index_fragments
.fragments
.insert(1, WebUIFragment::component("app-shell"));
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
protocol.components.insert(
"app-shell".to_string(),
webui_protocol::ComponentData {
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["name".to_string()],
..Default::default()
},
);
let state = test_json!({
"name": "Alice",
"tokens": {
"light": "--color-brand: red;"
}
});
let mut writer = TestWriter::new();
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
handler.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)?;
let output = writer.get_content();
assert!(output.contains("--color-brand: red;"));
assert!(output.contains(r#""name":"Alice""#));
assert!(!output.contains(r#""tokens""#));
Ok(())
}
#[test]
fn full_initial_strategy_preserves_complete_state() -> Result<()> {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body>"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"client": "visible",
"serverOnly": "also preserved",
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = TestWriter::new();
handler.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)?;
let output = writer.get_content();
assert!(output.contains(r#""client":"visible""#));
assert!(output.contains(r#""serverOnly":"also preserved""#));
Ok(())
}
#[test]
fn uncertain_hydration_surface_preserves_complete_state() -> Result<()> {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body>"),
WebUIFragment::component("app-shell"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Shell</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
protocol.components.insert(
"app-shell".to_string(),
webui_protocol::ComponentData {
hydration_mode: StateProjectionMode::All as i32,
..Default::default()
},
);
let state = test_json!({
"known": "value",
"possiblyInherited": "must not be dropped",
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = TestWriter::new();
handler.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)?;
let output = writer.get_content();
assert!(output.contains(r#""known":"value""#));
assert!(output.contains(r#""possiblyInherited":"must not be dropped""#));
Ok(())
}
#[test]
fn missing_component_projection_metadata_preserves_complete_state() -> Result<()> {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body>"),
WebUIFragment::component("app-shell"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Shell</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
let state = test_json!({
"known": "value",
"serverOnly": "must not be dropped",
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = TestWriter::new();
handler.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)?;
let output = writer.get_content();
assert!(output.contains(r#""known":"value""#));
assert!(output.contains(r#""serverOnly":"must not be dropped""#));
Ok(())
}
#[test]
fn unknown_projection_mode_preserves_complete_state() -> Result<()> {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body>"),
WebUIFragment::component("app-shell"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Shell</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
protocol.components.insert(
"app-shell".to_string(),
webui_protocol::ComponentData {
hydration_mode: i32::MAX,
..Default::default()
},
);
let state = test_json!({
"known": "value",
"serverOnly": "must not be dropped",
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = TestWriter::new();
handler.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)?;
let output = writer.get_content();
assert!(output.contains(r#""known":"value""#));
assert!(output.contains(r#""serverOnly":"must not be dropped""#));
Ok(())
}
#[test]
fn legacy_navigation_keys_with_default_mode_remain_keyed() {
let mut protocol = WebUIProtocol::new(HashMap::new());
protocol.components.insert(
"app-shell".to_string(),
webui_protocol::ComponentData {
navigation_keys: vec!["selected".to_string()],
..Default::default()
},
);
match collect_navigation_state(&protocol, ["app-shell"]) {
StateSelection::Keys(keys) => assert_eq!(keys, vec!["selected"]),
StateSelection::Full
| StateSelection::KeyIds(_)
| StateSelection::FullExceptKeyIds(_) => {
panic!("legacy navigation keys should remain owned and projected")
}
}
}
#[test]
fn legacy_navigation_without_projection_metadata_preserves_full_state() {
let mut protocol = WebUIProtocol::new(HashMap::new());
protocol.components.insert(
"app-shell".to_string(),
webui_protocol::ComponentData::default(),
);
assert!(matches!(
collect_navigation_state(&protocol, ["app-shell"]),
StateSelection::Full
));
}
#[test]
fn empty_reachable_hydration_keys_exclude_all_state() -> Result<()> {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body>".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
let state = test_json!({
"title": "Legacy state",
"serverOnly": "preserved",
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = TestWriter::new();
handler.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)?;
assert!(writer.get_content().contains(r#""state":{}"#));
assert!(!writer.get_content().contains("Legacy state"));
assert!(!writer.get_content().contains("preserved"));
Ok(())
}
#[test]
fn scriptless_component_state_is_navigation_only() -> Result<()> {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body>"),
WebUIFragment::component("items-page"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
fragments.insert(
"items-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Items</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
protocol.components.insert(
"items-page".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<p>Items</p>","th":1}"#.into(),
navigation_mode: Some(StateProjectionMode::Keys as i32),
navigation_keys: vec!["items".into()],
..Default::default()
},
);
let state = test_json!({
"items": ["STATE_SENTINEL"],
"serverOnly": "SECRET_SENTINEL",
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = TestWriter::new();
handler.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)?;
let output = writer.get_content();
assert!(output.contains(r#""state":{}"#));
assert!(!output.contains("STATE_SENTINEL"));
assert!(!output.contains("SECRET_SENTINEL"));
Ok(())
}
#[test]
fn write_selected_state_projects_and_escapes() {
let state = test_json!({
"drop": "secret",
"keep": "</script><b>"
});
let keys = ["keep"];
let selection = StateSelection::Keys(keys.to_vec());
let mut sink = TestWriter::new();
let mut scratch = Vec::new();
write_selected_state(&mut sink, &mut scratch, &state, &selection).unwrap();
assert_eq!(sink.get_content(), r#"{"keep":"<\/script><b>"}"#);
}
#[test]
fn write_selected_state_non_object_projection_emits_empty_object() {
let state = test_json!("scalar state has nothing hydratable");
let keys: [&str; 0] = [];
let selection = StateSelection::Keys(keys.to_vec());
let mut sink = TestWriter::new();
let mut scratch = Vec::new();
write_selected_state(&mut sink, &mut scratch, &state, &selection).unwrap();
assert_eq!(sink.get_content(), "{}");
}
#[test]
fn write_selected_state_schema_first_skips_missing_and_duplicate_keys() {
let state = test_json!({
"keptA": 1,
"keptB": 2,
"serverOnlyA": 3,
"serverOnlyB": 4,
});
let keys = ["keptA", "keptA", "keptB", "missing"];
let selection = StateSelection::Keys(keys.to_vec());
let mut sink = TestWriter::new();
let mut scratch = Vec::new();
write_selected_state(&mut sink, &mut scratch, &state, &selection).unwrap();
assert_eq!(sink.get_content(), r#"{"keptA":1,"keptB":2}"#);
}
#[test]
fn write_selected_state_map_first_matches_schema_first_output() {
let state = test_json!({
"keptA": 1,
"keptB": 2,
});
let keys = ["keptA", "keptB", "missingA", "missingB"];
let selection = StateSelection::Keys(keys.to_vec());
let mut sink = TestWriter::new();
let mut scratch = Vec::new();
write_selected_state(&mut sink, &mut scratch, &state, &selection).unwrap();
assert_eq!(sink.get_content(), r#"{"keptA":1,"keptB":2}"#);
}
#[test]
fn write_selected_state_full_preserves_and_escapes_state() {
let state = test_json!({
"serverOnly": "</script><b>",
"value": 42,
});
let mut sink = TestWriter::new();
let mut scratch = Vec::new();
write_selected_state(&mut sink, &mut scratch, &state, &StateSelection::Full).unwrap();
assert_eq!(
sink.get_content(),
r#"{"serverOnly":"<\/script><b>","value":42}"#
);
}
#[test]
fn script_safe_json_str_escapes_only_end_tag_openers() -> Result<()> {
let cases = [
("", ""),
("</", "<\\/"),
("</a</b", "<\\/a<\\/b"),
("<<//", "<<\\//"),
("a<b>c<", "a<b>c<"),
("x</", "x<\\/"),
("a<b>c</d>e</f", "a<b>c<\\/d>e<\\/f"),
("<img src=\"/a.png\"><br/>", "<img src=\"/a.png\"><br/>"),
];
for (json, expected) in cases {
let mut sink = TestWriter::new();
write_script_safe_json_str(&mut sink, json)?;
assert_eq!(sink.get_content(), expected, "input {json:?}");
}
Ok(())
}
#[test]
fn script_safe_json_str_finds_end_tag_openers_at_every_offset() -> Result<()> {
let mut json = String::with_capacity(512);
for gap in 0..130 {
json.clear();
json.push_str("{\"h\":\"<img src=\\\"/a.png\\\">");
json.push_str(&"x".repeat(gap));
json.push_str("</p><br/>");
json.push_str(&"y".repeat(gap % 37));
json.push_str("</");
let mut sink = TestWriter::new();
write_script_safe_json_str(&mut sink, &json)?;
assert_eq!(sink.get_content(), json.replace("</", "<\\/"), "gap {gap}");
}
Ok(())
}
#[test]
fn component_asset_style_manifest_is_serialized_once_and_script_safe() {
let preloads = vec![ComponentAssetStylePreload {
root: "lazy-panel".to_string(),
style_hrefs: vec!["/assets/</script><script>alert(1)</script>.css".to_string()],
}];
let manifest = serialize_component_asset_style_manifest(&preloads).unwrap();
assert_eq!(
manifest,
r#"{"lazy-panel":["/assets/<\/script><script>alert(1)<\/script>.css"]}"#
);
}
#[test]
fn json_scratch_capacity_reused_across_checkpoint_payloads() {
let mut sink = TestWriter::new();
let mut scratch: Vec<u8> = Vec::new();
assert_eq!(scratch.capacity(), 0);
let mut large = serde_json::Map::new();
for index in 0..64 {
large.insert(
format!("component_{index:04}"),
Value::String("x".repeat(48)),
);
}
let large = Value::Object(large);
write_script_safe_json(&mut sink, &mut scratch, &large).unwrap();
let high_water = scratch.capacity();
assert!(high_water > 0);
for seq in 0..8 {
let small = test_json!({ "inventory": "", "state": {}, "seq": seq });
write_script_safe_json(&mut sink, &mut scratch, &small).unwrap();
assert_eq!(
scratch.capacity(),
high_water,
"scratch reallocated on checkpoint {seq}"
);
}
}
#[test]
fn bootstrap_state_excludes_inactive_route_hydration_keys() -> Result<()> {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body>"),
WebUIFragment::route_from(webui_protocol::WebUiFragmentRoute {
path: "/".to_string(),
fragment_id: "home-page".to_string(),
exact: true,
..Default::default()
}),
WebUIFragment::route_from(webui_protocol::WebUiFragmentRoute {
path: "/admin".to_string(),
fragment_id: "admin-page".to_string(),
exact: true,
..Default::default()
}),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
fragments.insert(
"home-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Home</p>")],
contains_boundary: false,
},
);
fragments.insert(
"admin-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Admin</p>")],
contains_boundary: false,
},
);
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
protocol.components.insert(
"home-page".to_string(),
webui_protocol::ComponentData {
template_json: "{}".to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["homeTitle".to_string()],
..Default::default()
},
);
protocol.components.insert(
"admin-page".to_string(),
webui_protocol::ComponentData {
template_json: "{}".to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["adminToken".to_string()],
..Default::default()
},
);
let state = test_json!({
"homeTitle": "Welcome",
"adminToken": "TOP_SECRET_SENTINEL",
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = TestWriter::new();
handler.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)?;
let output = writer.get_content();
assert!(output.contains(r#""homeTitle":"Welcome""#));
assert!(!output.contains("TOP_SECRET_SENTINEL"));
assert!(!output.contains(r#""adminToken""#));
Ok(())
}
#[test]
fn test_component_attr_name_aria() {
assert_eq!(component_attr_name("aria-describedby"), "ariaDescribedBy");
assert_eq!(component_attr_name("aria-labelledby"), "ariaLabelledBy");
assert_eq!(
component_attr_name("aria-activedescendant"),
"ariaActiveDescendant"
);
assert_eq!(component_attr_name("aria-label"), "ariaLabel");
assert_eq!(component_attr_name("aria-hidden"), "ariaHidden");
}
#[test]
fn test_component_attr_name_html_global() {
assert_eq!(component_attr_name("readonly"), "readOnly");
assert_eq!(component_attr_name("tabindex"), "tabIndex");
assert_eq!(component_attr_name("accesskey"), "accessKey");
assert_eq!(component_attr_name("contenteditable"), "contentEditable");
assert_eq!(component_attr_name("crossorigin"), "crossOrigin");
assert_eq!(component_attr_name("inputmode"), "inputMode");
assert_eq!(component_attr_name("maxlength"), "maxLength");
assert_eq!(component_attr_name("minlength"), "minLength");
assert_eq!(component_attr_name("novalidate"), "noValidate");
assert_eq!(component_attr_name("formaction"), "formAction");
assert_eq!(component_attr_name("ismap"), "isMap");
assert_eq!(component_attr_name("usemap"), "useMap");
}
#[test]
fn test_component_attr_name_strips_colon() {
assert_eq!(component_attr_name(":readonly"), "readOnly");
assert_eq!(component_attr_name(":aria-describedby"), "ariaDescribedBy");
assert_eq!(component_attr_name(":data-title"), "dataTitle");
}
#[test]
fn test_component_attr_name_regular() {
assert_eq!(component_attr_name("data-title"), "dataTitle");
assert_eq!(component_attr_name("key-hyphen"), "keyHyphen");
assert_eq!(component_attr_name("simple"), "simple");
}
fn make_query_route_protocol() -> WebUIProtocol {
use webui_protocol::WebUiFragmentRoute;
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(WebUiFragmentRoute {
path: "/".into(),
fragment_id: "app-shell".into(),
exact: false,
children: vec![
WebUiFragmentRoute {
path: "compose".into(),
fragment_id: "compose-page".into(),
exact: true,
allowed_query: "action,to,subject".into(),
keep_alive: false,
..Default::default()
},
WebUiFragmentRoute {
path: "settings".into(),
fragment_id: "settings-page".into(),
exact: true,
keep_alive: false,
..Default::default()
},
],
keep_alive: false,
..Default::default()
})],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<h1>App</h1>"), WebUIFragment::outlet()],
contains_boundary: false,
},
);
fragments.insert(
"compose-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Compose</p>")],
contains_boundary: false,
},
);
fragments.insert(
"settings-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Settings</p>")],
contains_boundary: false,
},
);
WebUIProtocol::new(fragments)
}
#[test]
fn test_matched_route_omits_query_attr_from_dom() {
let protocol = make_query_route_protocol();
let state = test_json!({});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/compose"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
assert!(
!html.contains(r#"query="action,to,subject""#),
"query attr should not be in DOM output (moved to SSR chain JSON): {html}"
);
}
#[test]
fn test_nonmatched_route_omits_query_attr_from_dom() {
let protocol = make_query_route_protocol();
let state = test_json!({});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/settings"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
assert!(
!html.contains(r#"query="#),
"hidden route should not have query attr: {html}"
);
}
#[test]
fn test_route_without_query_has_no_query_attr() {
let protocol = make_query_route_protocol();
let state = test_json!({});
let handler = WebUIHandler::new();
let mut writer = TestWriter::new();
handler
.handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/settings"),
&mut writer,
)
.expect("render failed");
let html = writer.get_content();
let settings_idx = html
.find(r#"component="settings-page""#)
.expect("settings route should exist");
let settings_tag = &html[settings_idx.saturating_sub(60)..settings_idx + 40];
assert!(
!settings_tag.contains("query="),
"route without allowed_query should not emit query attr: {settings_tag}"
);
}
fn build_head_body_protocol() -> WebUIProtocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head><title>x</title>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>hello".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
WebUIProtocol::new(fragments)
}
#[test]
fn head_inject_emits_at_head_end_boundary() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/").with_head_inject("<link rel=preload>");
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
let inject_idx = html
.find("<link rel=preload>")
.expect("inject HTML missing");
let head_close_idx = html.find("</head>").expect("</head> missing");
assert!(
inject_idx < head_close_idx,
"head_inject must appear before </head>: {html}"
);
assert_eq!(html.matches("<link rel=preload>").count(), 1);
}
#[test]
fn component_render_policy_css_emits_once_in_head_with_nonce() {
let mut protocol = build_head_body_protocol();
protocol.component_render_css = concat!(
r#"lazy-card:not([w-render="eager"]){content-visibility:auto;"#,
"contain-intrinsic-block-size:auto 18rem;}"
)
.to_string();
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/").with_nonce("test-nonce");
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
let style = concat!(
r#"<style data-webui-render-policy nonce="test-nonce">"#,
r#"lazy-card:not([w-render="eager"]){content-visibility:auto;"#,
"contain-intrinsic-block-size:auto 18rem;}</style>"
);
let style_index = html.find(style).expect("render policy style missing");
let head_close = html.find("</head>").expect("</head> missing");
assert!(style_index < head_close);
assert_eq!(html.matches("data-webui-render-policy").count(), 1);
}
#[test]
fn body_inject_emits_at_body_end_boundary() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/").with_body_inject("<script>lr</script>");
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
let inject_idx = html
.find("<script>lr</script>")
.expect("inject HTML missing");
let body_close_idx = html.find("</body>").expect("</body> missing");
assert!(
inject_idx < body_close_idx,
"body_inject must appear before </body>: {html}"
);
assert_eq!(html.matches("<script>lr</script>").count(), 1);
}
fn build_all_boundaries_protocol() -> WebUIProtocol {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head><title>x</title>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>".to_string()),
structural_fragment("body_start"),
WebUIFragment::raw("hello".to_string()),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
WebUIProtocol::new(fragments)
}
fn state_inject_options<'a>() -> RenderOptions<'a> {
RenderOptions::new("index.html", "/")
}
fn render_with(protocol: &WebUIProtocol, state: &Value, options: &RenderOptions<'_>) -> String {
let mut writer = TestWriter::new();
handle(protocol, state, options, &mut writer).unwrap();
writer.get_content().to_string()
}
#[test]
fn state_inject_emits_at_every_structural_boundary() {
let protocol = build_all_boundaries_protocol();
let state = test_json!({
"$webui": {
"headEnd": "<meta name=he>",
"bodyStart": "<span id=bs></span>",
"bodyEnd": "<script>be</script>",
}
});
let html = render_with(&protocol, &state, &state_inject_options());
let head_end = html.find("<meta name=he>").expect("headEnd missing");
let head_close = html.find("</head>").expect("</head> missing");
let body_open = html.find("<body>").expect("<body> missing");
let body_start = html.find("<span id=bs></span>").expect("bodyStart missing");
let hello = html.find("hello").expect("body content missing");
let body_end = html.find("<script>be</script>").expect("bodyEnd missing");
let body_close = html.find("</body>").expect("</body> missing");
assert!(
head_end < head_close,
"headEnd must precede </head>: {html}"
);
assert!(
body_open < body_start && body_start < hello,
"bodyStart must sit immediately after <body>: {html}"
);
assert!(
hello < body_end && body_end < body_close,
"bodyEnd must precede </body>: {html}"
);
for needle in [
"<meta name=he>",
"<span id=bs></span>",
"<script>be</script>",
] {
assert_eq!(html.matches(needle).count(), 1, "duplicated {needle}");
}
}
#[test]
fn state_inject_follows_render_options_inject() {
let protocol = build_all_boundaries_protocol();
let state = test_json!({
"$webui": { "headEnd": "<!--state-he-->", "bodyEnd": "<!--state-be-->" }
});
let options = RenderOptions::new("index.html", "/")
.with_head_inject("<!--opt-he-->")
.with_body_inject("<!--opt-be-->");
let html = render_with(&protocol, &state, &options);
let opt_he = html.find("<!--opt-he-->").expect("option headEnd missing");
let state_he = html.find("<!--state-he-->").expect("state headEnd missing");
let opt_be = html.find("<!--opt-be-->").expect("option bodyEnd missing");
let state_be = html.find("<!--state-be-->").expect("state bodyEnd missing");
assert!(
opt_he < state_he,
"RenderOptions head_inject must precede the state-supplied value: {html}"
);
assert!(
opt_be < state_be,
"RenderOptions body_inject must precede the state-supplied value: {html}"
);
}
#[test]
fn malformed_state_inject_values_are_inert() {
let protocol = build_all_boundaries_protocol();
for state in [
test_json!({}),
test_json!({ "$webui": "not-an-object" }),
test_json!({ "$webui": [] }),
test_json!({ "$webui": null }),
test_json!({ "$webui": { "headEnd": null, "bodyStart": "", "bodyEnd": 42 } }),
test_json!({ "$webui": { "unknownMember": "<b>x</b>" } }),
] {
let html = render_with(&protocol, &state, &state_inject_options());
assert!(
html.contains("<html><head><title>x</title></head><body>hello</body></html>"),
"malformed reserved state must render the document unchanged: {html}"
);
assert!(!html.contains("<b>x</b>"), "unknown member leaked: {html}");
assert!(!html.contains("42"), "non-string member leaked: {html}");
}
}
#[test]
fn state_inject_never_reaches_the_hydration_payload() {
let protocol = build_all_boundaries_protocol();
let state = test_json!({
"visible": "keep",
"$webui": { "bodyEnd": "<script>secret</script>" }
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let runtime = Protocol::new(protocol.clone());
let mut writer = TestWriter::new();
handler
.render(&runtime, &state, &state_inject_options(), &mut writer)
.unwrap();
let html = writer.get_content();
let data_start = html
.find(r#"<script type="application/json" id="webui-data""#)
.expect("hydration block missing");
let data_end = html[data_start..]
.find("</script>")
.map(|offset| data_start + offset)
.expect("hydration block never closes");
let payload = &html[data_start..data_end];
assert!(
!payload.contains("$webui"),
"reserved key must be stripped from the hydration payload: {payload}"
);
assert!(
payload.contains("visible"),
"ordinary state must survive the filter: {payload}"
);
let inject = html
.find("<script>secret</script>")
.expect("inject missing");
let body_close = html.find("</body>").expect("</body> missing");
assert!(
data_end < inject && inject < body_close,
"state bodyEnd must follow the hydration block and precede </body>: {html}"
);
}
#[test]
fn projected_hydration_strips_reserved_state_inject_key() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><body>"),
WebUIFragment::component("app-shell"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
);
fragments.insert(
"app-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>Shell</p>")],
contains_boundary: false,
},
);
let mut document = WebUIProtocol::new(fragments);
document.initial_state_strategy = InitialStateStrategy::Components as i32;
document.components.insert(
"app-shell".to_string(),
webui_protocol::ComponentData {
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec![STATE_INJECT_KEY.to_string(), "visible".to_string()],
..Default::default()
},
);
let state = test_json!({
"$webui": { "bodyEnd": "<script>secret</script>" },
"serverOnly": "drop",
"visible": "keep",
});
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = TestWriter::new();
handler
.render(
&Protocol::new(document),
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
let data_start = html
.find(r#"<script type="application/json" id="webui-data""#)
.expect("hydration block missing");
let data_end = html[data_start..]
.find("</script>")
.map(|offset| data_start + offset)
.expect("hydration block never closes");
let payload = &html[data_start..data_end];
assert!(
!payload.contains(STATE_INJECT_KEY),
"reserved key leaked: {payload}"
);
assert!(payload.contains(r#""visible":"keep""#), "{payload}");
assert!(!payload.contains("serverOnly"), "{payload}");
assert!(html.contains("<script>secret</script>"), "{html}");
}
#[test]
fn write_selected_state_strips_reserved_key_from_full_state() {
let state = test_json!({ "a": 1, "$webui": { "bodyEnd": "<b>x</b>" }, "z": 2 });
let mut sink = TestWriter::new();
let mut scratch = Vec::new();
write_selected_state(&mut sink, &mut scratch, &state, &StateSelection::Full).unwrap();
let json = sink.get_content();
assert!(!json.contains("$webui"), "reserved key leaked: {json}");
assert!(
json.contains("\"a\":1") && json.contains("\"z\":2"),
"{json}"
);
}
#[test]
fn write_selected_state_strips_reserved_key_from_key_id_projection() {
let mut protocol = WebUIProtocol::new(HashMap::new());
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
protocol.fragments.insert(
"keep-card".to_string(),
webui_protocol::FragmentList::default(),
);
protocol.components.insert(
"keep-card".to_string(),
webui_protocol::ComponentData {
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec![STATE_INJECT_KEY.to_string(), "keep".to_string()],
..Default::default()
},
);
let protocol = Protocol::new(protocol);
let index = protocol.component_reachability();
let component = protocol.component_index()["keep-card"];
let mut ids = Vec::new();
assert!(!collect_hydration_key_ids_into(
protocol.protocol(),
index,
[component],
&mut ids
));
let state = test_json!({
"$webui": { "bodyEnd": "<b>x</b>" },
"keep": 1,
});
let mut sink = TestWriter::new();
let mut scratch = Vec::new();
write_selected_state(
&mut sink,
&mut scratch,
&state,
&StateSelection::KeyIds(HydrationKeySelection { ids: &ids, index }),
)
.unwrap();
assert_eq!(sink.get_content(), r#"{"keep":1}"#);
}
#[test]
fn write_selected_state_full_is_unchanged_without_reserved_key() {
let state = test_json!({ "a": 1, "z": 2 });
let mut sink = TestWriter::new();
let mut scratch = Vec::new();
write_selected_state(&mut sink, &mut scratch, &state, &StateSelection::Full).unwrap();
assert_eq!(sink.get_content(), r#"{"a":1,"z":2}"#);
}
#[test]
fn body_start_hook_dedupes_on_malformed_protocol() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head></head><body>".to_string()),
structural_fragment("body_start"),
structural_fragment("body_start"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({ "$webui": { "bodyStart": "<i>once</i>" } });
let html = render_with(&protocol, &state, &state_inject_options());
assert_eq!(
html.matches("<i>once</i>").count(),
1,
"duplicate body_start signals must not duplicate the inject: {html}"
);
}
#[test]
fn injects_are_no_op_when_unset() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let html = writer.get_content();
assert!(!html.contains("<link rel=preload>"));
assert!(!html.contains("<script>lr</script>"));
}
#[test]
fn empty_inject_string_treated_as_unset() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/")
.with_head_inject("")
.with_body_inject("");
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
assert!(html.contains("</head>"));
assert!(html.contains("</body>"));
}
#[test]
fn inject_html_is_passed_through_verbatim() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
let opts =
RenderOptions::new("index.html", "/").with_body_inject("<script>var x=1;</script>");
handle(&protocol, &state, &opts, &mut writer).unwrap();
assert!(writer.get_content().contains("<script>var x=1;</script>"));
}
#[test]
fn injects_robust_against_marker_literals_in_content() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head><title>x</title>".to_string()),
structural_fragment("head_end"),
WebUIFragment::raw(
"</head><body><!-- </body> </head> --><p>hi</p>".to_string(),
),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/")
.with_head_inject("<HEAD-INJ>")
.with_body_inject("<BODY-INJ>");
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
let head_inj_idx = html.find("<HEAD-INJ>").expect("head inject missing");
let head_close_idx = html.find("</head>").expect("</head> missing");
assert!(head_inj_idx < head_close_idx);
let body_inj_idx = html.find("<BODY-INJ>").expect("body inject missing");
let body_close_idx = html.rfind("</body>").expect("</body> missing");
assert!(
body_inj_idx < body_close_idx,
"body_inject must precede the structural </body>: {html}"
);
assert!(html.contains("<!-- </body> </head> -->"));
}
#[test]
fn both_injects_fire_at_correct_boundaries() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/")
.with_head_inject("<META-HEAD>")
.with_body_inject("<SCRIPT-BODY>");
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
let head_idx = html.find("<META-HEAD>").expect("head inject missing");
let head_close = html.find("</head>").expect("</head> missing");
let body_idx = html.find("<SCRIPT-BODY>").expect("body inject missing");
let body_close = html.find("</body>").expect("</body> missing");
assert!(head_idx < head_close, "head_inject before </head>");
assert!(head_close < body_idx, "body_inject after </head>");
assert!(body_idx < body_close, "body_inject before </body>");
assert_eq!(html.matches("<META-HEAD>").count(), 1);
assert_eq!(html.matches("<SCRIPT-BODY>").count(), 1);
}
#[test]
fn injects_dedupe_against_duplicate_signals() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>".to_string()),
structural_fragment("head_end"),
structural_fragment("head_end"), structural_fragment("head_end"), WebUIFragment::raw("</head><body>".to_string()),
structural_fragment("body_end"),
structural_fragment("body_end"), WebUIFragment::raw("</body></html>".to_string()),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/")
.with_head_inject("<HINJ>")
.with_body_inject("<BINJ>");
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
assert_eq!(
html.matches("<HINJ>").count(),
1,
"head_inject must emit exactly once even with duplicate head_end signals"
);
assert_eq!(
html.matches("<BINJ>").count(),
1,
"body_inject must emit exactly once even with duplicate body_end signals"
);
}
#[test]
fn injects_no_op_when_no_head_or_body_signals() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw(
"<my-component>hi</my-component>".to_string(),
)],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/")
.with_head_inject("<HINJ>")
.with_body_inject("<BINJ>");
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
assert!(!html.contains("<HINJ>"), "head_inject must not appear");
assert!(!html.contains("<BINJ>"), "body_inject must not appear");
assert!(html.contains("<my-component>"));
}
#[test]
fn concurrent_renders_with_different_injects_do_not_cross_contaminate() {
let protocol = std::sync::Arc::new(build_head_body_protocol());
let state = std::sync::Arc::new(test_json!({}));
let handler = std::sync::Arc::new(WebUIHandler::new());
const N_THREADS: usize = 16;
let mut handles = Vec::with_capacity(N_THREADS);
for tid in 0..N_THREADS {
let h = std::sync::Arc::clone(&handler);
let p = std::sync::Arc::clone(&protocol);
let s = std::sync::Arc::clone(&state);
handles.push(std::thread::spawn(move || {
let head = format!("<HEAD-T{tid}>");
let body = format!("<BODY-T{tid}>");
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/")
.with_head_inject(&head)
.with_body_inject(&body);
h.handle(&p, &s, &opts, &mut writer).unwrap();
let html = writer.get_content();
assert_eq!(html.matches(&head).count(), 1);
assert_eq!(html.matches(&body).count(), 1);
for other in 0..N_THREADS {
if other == tid {
continue;
}
let other_head = format!("<HEAD-T{other}>");
let other_body = format!("<BODY-T{other}>");
assert!(
!html.contains(&other_head),
"tid {tid} saw {other}'s head_inject"
);
assert!(
!html.contains(&other_body),
"tid {tid} saw {other}'s body_inject"
);
}
}));
}
for h in handles {
h.join().expect("worker panicked");
}
}
#[test]
fn large_inject_roundtrips_without_truncation() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
let big = "x".repeat(1024 * 1024);
let opts = RenderOptions::new("index.html", "/").with_head_inject(&big);
handle(&protocol, &state, &opts, &mut writer).unwrap();
let html = writer.get_content();
assert!(
html.contains(&big),
"large head_inject must be present verbatim ({} bytes)",
big.len()
);
assert_eq!(html.matches(&big).count(), 1);
}
#[test]
fn empty_nonce_treated_as_unset() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let mut writer = TestWriter::new();
let opts = RenderOptions::new("index.html", "/").with_nonce("");
handle(&protocol, &state, &opts, &mut writer).unwrap();
assert!(
!writer.get_content().contains("webui-nonce"),
"empty nonce must not emit <meta name=\"webui-nonce\">"
);
}
#[test]
fn empty_field_bypass_is_normalised_at_handler_init() {
let protocol = build_head_body_protocol();
let state = test_json!({});
let opts_with_empty_nonce = RenderOptions {
nonce: Some(""),
..RenderOptions::new("index.html", "/")
};
let mut writer = TestWriter::new();
handle(&protocol, &state, &opts_with_empty_nonce, &mut writer).unwrap();
let html = writer.get_content();
assert!(
!html.contains("webui-nonce"),
"field-bypass empty nonce must not emit `<meta name=\"webui-nonce\">`"
);
assert!(
!html.contains("nonce=\"\""),
"field-bypass empty nonce must not emit `nonce=\"\"` (would be a hard CSP failure)"
);
let opts_with_empty_injects = RenderOptions {
head_inject: Some(""),
body_inject: Some(""),
..RenderOptions::new("index.html", "/")
};
let mut writer = TestWriter::new();
handle(&protocol, &state, &opts_with_empty_injects, &mut writer).unwrap();
}
#[test]
fn nested_for_loops_reusing_same_variable_name_dont_corrupt_scope() {
let mut fragments = HashMap::new();
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("["),
WebUIFragment::for_loop("item", "outer", "outer_body"),
WebUIFragment::raw("]"),
],
contains_boundary: false,
},
);
fragments.insert(
"outer_body".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("(O="),
WebUIFragment::signal("item.tag", false),
WebUIFragment::for_loop("item", "inner", "inner_body"),
WebUIFragment::raw(",O="),
WebUIFragment::signal("item.tag", false),
WebUIFragment::raw(")"),
],
contains_boundary: false,
},
);
fragments.insert(
"inner_body".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("[I="),
WebUIFragment::signal("item.tag", false),
WebUIFragment::raw("]"),
],
contains_boundary: false,
},
);
let protocol = WebUIProtocol::new(fragments);
let state = test_json!({
"outer": [{"tag": "A"}, {"tag": "B"}],
"inner": [{"tag": "X"}, {"tag": "Y"}],
});
let mut writer = TestWriter::new();
handle(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"[(O=A[I=X][I=Y],O=A)(O=B[I=X][I=Y],O=B)]",
"outer `item` must stay bound to its iteration value across the inner loop's save/restore"
);
}
#[derive(Default)]
struct FlushTestWriter {
output: String,
flushes: Vec<usize>,
fail_flush: bool,
fail_flush_at: Option<usize>,
flush_attempts: usize,
ended: bool,
}
impl ResponseWriter for FlushTestWriter {
fn write(&mut self, content: &str) -> Result<()> {
self.output.push_str(content);
Ok(())
}
fn end(&mut self) -> Result<()> {
self.ended = true;
Ok(())
}
}
impl FlushWriter for FlushTestWriter {
fn flush(&mut self) -> Result<()> {
let attempt = self.flush_attempts;
self.flush_attempts += 1;
if self.fail_flush || self.fail_flush_at == Some(attempt) {
return Err(HandlerError::ClientDisconnected);
}
self.flushes.push(self.output.len());
Ok(())
}
}
fn streaming_protocol(with_boundaries: bool) -> Protocol {
streaming_protocol_with_state_strategy(with_boundaries, InitialStateStrategy::Components)
}
fn streaming_protocol_with_state_strategy(
with_boundaries: bool,
state_strategy: InitialStateStrategy,
) -> Protocol {
let mut fragments = HashMap::new();
let mut entry = vec![
WebUIFragment::raw("<!DOCTYPE html><html><HEAD data-shell=\"main\">"),
structural_fragment("head_start"),
WebUIFragment::raw("<script type=\"module\" async src=\"/index.js\"></script>"),
structural_fragment("head_end"),
WebUIFragment::raw("</HEAD><body>"),
structural_fragment("body_start"),
];
if with_boundaries {
entry.push(WebUIFragment::boundary(0, "index.html", "boundary-0", None));
}
entry.extend([
WebUIFragment::raw("<my-counter"),
structural_fragment("streaming_root:my-counter"),
WebUIFragment::raw(">"),
WebUIFragment::component("my-counter"),
WebUIFragment::raw("</my-counter>"),
]);
if with_boundaries {
entry.push(WebUIFragment::boundary_end(0));
}
entry.extend([
WebUIFragment::raw("<app-footer>slow tail</app-footer>"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
]);
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: entry,
contains_boundary: with_boundaries,
},
);
fragments.insert(
"my-counter".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<button>Count</button>")],
contains_boundary: false,
},
);
let mut document = WebUIProtocol::new(fragments);
document.initial_state_strategy = state_strategy as i32;
document.components.insert(
"my-counter".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<button>Count</button>","th":1}"#.to_string(),
template_functions: "[function(){return true}]".to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["count".to_string()],
..Default::default()
},
);
Protocol::new(document)
}
#[test]
fn handler_error_stays_small() {
assert!(
std::mem::size_of::<HandlerError>()
<= std::mem::size_of::<String>() + std::mem::size_of::<usize>(),
"HandlerError grew to {} bytes",
std::mem::size_of::<HandlerError>()
);
}
#[test]
fn streaming_render_flushes_bootstrap_before_slow_tail_and_emits_terminal() {
let protocol = streaming_protocol(true);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let state = test_json!({ "count": 1, "serverOnly": "secret" });
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert!(writer.ended);
assert_eq!(
writer.flushes.len(),
3,
"bootstrap, boundary commit, and one coalesced terminal-tail flush"
);
let boundary_flush = &writer.output[writer.flushes[0]..writer.flushes[1]];
assert!(boundary_flush.contains("<!--wb:0-->"));
assert!(boundary_flush.contains("<!--/wb:0-->"));
assert!(
boundary_flush.contains(r#"[0,0,0,{"declarationId":0,"componentStyles":"#),
"boundary flush: {boundary_flush}"
);
assert!(boundary_flush.contains(r#""inventory":"01","state":{"count":1}"#));
assert!(boundary_flush.contains(r#""templates":{"my-counter":"#));
assert!(!boundary_flush.contains("slow tail"));
assert!(!writer.output.contains("id=\"webui-data\""));
let terminal_flush = &writer.output[writer.flushes[1]..writer.flushes[2]];
assert!(terminal_flush.contains("slow tail"));
assert!(writer.output.contains("[1,4,0,{}]"));
assert!(!writer.output.contains("[1,0,1,"));
assert!(!writer.output.contains("[2,4,0,{}]"));
let marker = writer
.output
.find(STREAMING_MARKER)
.expect("streaming marker");
let attributed_head = writer
.output
.find("<HEAD data-shell=\"main\">")
.expect("attributed mixed-case head");
let authored_script = writer
.output
.find("src=\"/index.js\"")
.expect("entry script");
assert!(attributed_head < marker && marker < authored_script);
}
#[test]
fn streaming_terminal_tail_never_resends_full_state() {
let protocol = streaming_protocol_with_state_strategy(true, InitialStateStrategy::Full);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&test_json!({ "count": 1, "serverOnly": "secret" }),
&RenderOptions::new("index.html", "/").with_body_inject(" \n"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.output.matches("serverOnly").count(),
1,
"full state belongs only to the interactive boundary"
);
assert!(writer.output.contains("[1,4,0,{}]"));
}
#[test]
fn streaming_terminal_record_emits_before_body_close() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::raw("<main>static</main>"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
)]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let mut writer = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let terminal = writer.output.find("[0,4,0,{}]").expect("terminal");
let body_close = writer.output.find("</body>").expect("body close");
assert!(
terminal < body_close,
"terminal boundary record must stay inside <body>: {}",
writer.output
);
}
#[test]
fn state_inject_placement_matches_between_render_modes() {
let entry = vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::raw("<main>static</main>"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
];
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: entry,
contains_boundary: false,
},
)]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let state = test_json!({
"$webui": {
"headEnd": "<meta name=he>",
"bodyStart": "<span id=bs></span>",
"bodyEnd": "<script>be</script>",
}
});
let options = RenderOptions::new("index.html", "/");
let mut ordinary = TestWriter::new();
WebUIHandler::new()
.render(&protocol, &state, &options, &mut ordinary)
.unwrap();
let ordinary_html = ordinary.get_content().to_string();
let mut streamed = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(&protocol, &state, &options, &mut streamed)
.unwrap();
let streamed_html = &streamed.output;
for html in [ordinary_html.as_str(), streamed_html.as_str()] {
let head_end = html.find("<meta name=he>").expect("headEnd missing");
let head_close = html.find("</head>").expect("</head> missing");
let body_start = html.find("<span id=bs></span>").expect("bodyStart missing");
let main = html.find("<main>static</main>").expect("content missing");
let body_end = html.find("<script>be</script>").expect("bodyEnd missing");
let body_close = html.find("</body>").expect("</body> missing");
assert!(head_end < head_close, "headEnd misplaced: {html}");
assert!(body_start < main, "bodyStart misplaced: {html}");
assert!(
main < body_end && body_end < body_close,
"bodyEnd misplaced: {html}"
);
}
assert!(
streamed_html.contains(",4,0,{}]"),
"streaming must still end in one empty terminal record: {streamed_html}"
);
}
#[test]
fn streaming_state_inject_emits_and_strips_reserved_key() {
let entry = vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
];
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: entry,
contains_boundary: false,
},
)]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let state = test_json!({ "$webui": { "bodyEnd": "<script>be</script>" } });
let mut writer = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert!(writer.output.contains("<script>be</script>"));
assert!(!writer.output.contains("$webui"));
}
#[test]
fn static_streaming_document_uses_one_empty_terminal_record() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::raw("<main>static</main>"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
)]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let mut writer = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(
&protocol,
&test_json!({ "serverOnly": "secret" }),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(writer.flushes.len(), 1);
assert!(writer.output.contains(STREAMING_MARKER));
assert!(writer.output.contains("[0,4,0,{}]"));
assert!(!writer.output.contains("serverOnly"));
assert!(!writer.output.contains("id=\"webui-data\""));
assert!(!writer.output.contains("<!--wb:"));
}
#[test]
fn streaming_render_emits_nonce_and_functions_before_sentinel() {
let protocol = streaming_protocol(true);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&test_json!({ "count": 1 }),
&RenderOptions::new("index.html", "/").with_nonce("test-nonce-123"),
&mut writer,
)
.unwrap();
let boundary_end = writer.output.find("<!--/wb:0-->").expect("end marker");
let envelope = writer.output[boundary_end..]
.find("data-webui-boundary nonce=\"test-nonce-123\"")
.map(|index| index + boundary_end)
.expect("nonce-bearing envelope");
let functions = writer
.output
.find("templateFns")
.expect("function side channel");
let sentinel = writer
.output
.find("<webui-hydrate>")
.expect("hydration sentinel");
assert!(boundary_end < envelope && envelope < functions && functions < sentinel);
assert!(writer.output.contains("<script nonce=\"test-nonce-123\">"));
}
#[test]
fn streaming_render_rejects_out_of_order_and_nested_boundaries() {
fn protocol(boundaries: Vec<WebUIFragment>) -> Protocol {
let mut fragments = vec![structural_fragment("head_start")];
fragments.extend(boundaries);
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments,
contains_boundary: true,
},
)]);
Protocol::new(WebUIProtocol::new(fragments))
}
let handler = WebUIHandler::new();
let mut writer = FlushTestWriter::default();
let result = handler.render_streaming(
&protocol(vec![WebUIFragment::boundary_end(0)]),
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
);
match result {
Err(HandlerError::StreamingBoundary(err)) => {
assert_eq!(err.signal, "boundary");
assert!(err.reason.contains("no active occurrence"));
}
other => panic!("expected ordered-boundary error, got {other:?}"),
}
let mut writer = FlushTestWriter::default();
let result = handler.render_streaming(
&protocol(vec![
WebUIFragment::boundary(0, "index.html", "boundary-0", None),
WebUIFragment::boundary(1, "index.html", "boundary-1", None),
]),
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
);
assert!(matches!(result, Err(HandlerError::StreamingBoundary(_))));
}
#[test]
fn streaming_render_propagates_boundary_flush_disconnect() {
let protocol = streaming_protocol(true);
let handler = WebUIHandler::new();
let mut writer = FlushTestWriter {
fail_flush: true,
..FlushTestWriter::default()
};
let result = handler.render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
);
assert!(matches!(result, Err(HandlerError::ClientDisconnected)));
assert!(!writer.ended);
}
#[test]
fn streaming_render_requires_structural_head_start_before_writing() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw(
"<html><HEAD data-shell=\"main\"><script src=\"/early.js\"></script>",
),
structural_fragment("head_end"),
WebUIFragment::raw("</HEAD><body>"),
structural_fragment("body_start"),
structural_fragment("body_end"),
],
contains_boundary: false,
},
)]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let mut writer = FlushTestWriter::default();
let result = WebUIHandler::new().render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
);
assert!(matches!(
result,
Err(HandlerError::MissingStreamingHeadStart { before: "head_end" })
));
assert_eq!(
writer.output,
"<html><HEAD data-shell=\"main\"><script src=\"/early.js\"></script>"
);
assert!(writer.flushes.is_empty());
assert!(!writer.ended);
}
#[test]
fn streaming_render_rejects_duplicate_head_start() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
structural_fragment("body_end"),
],
contains_boundary: false,
},
)]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let mut writer = FlushTestWriter::default();
let result = WebUIHandler::new().render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
);
assert!(matches!(
result,
Err(HandlerError::DuplicateStreamingHeadStart)
));
assert_eq!(writer.output.matches(STREAMING_MARKER).count(), 1);
assert!(writer.flushes.is_empty());
}
#[test]
fn legacy_render_is_byte_identical_when_boundary_signals_are_present() {
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let state = test_json!({
"count": 1,
"head_start": "must not render",
"boundary_start:0": "must not render",
"boundary_end:0": "must not render",
});
let mut with_boundaries = TestWriter::new();
let mut without_boundaries = TestWriter::new();
handler
.render(
&streaming_protocol(true),
&state,
&RenderOptions::new("index.html", "/"),
&mut with_boundaries,
)
.unwrap();
handler
.render(
&streaming_protocol(false),
&state,
&RenderOptions::new("index.html", "/"),
&mut without_boundaries,
)
.unwrap();
assert_eq!(
with_boundaries.get_content(),
without_boundaries.get_content()
);
}
#[test]
fn parser_head_attributes_preserve_legacy_ordinary_render_bytes() {
for source in [
r#"<html><head data-theme="dark"><title>T</title></head><body>x</body></html>"#,
r#"<html><head data-theme="{{theme}}"><title>T</title></head><body>x</body></html>"#,
] {
let mut parser = HtmlParser::new();
parser
.parse("index.html", source)
.expect("parse head fixture");
let protocol = Protocol::new(WebUIProtocol::new(parser.into_fragment_records()));
let mut writer = TestWriter::new();
WebUIHandler::new()
.render(
&protocol,
&test_json!({ "theme": "light" }),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.get_content(),
"<html><head><title>T</title></head><body>x</body></html>"
);
}
}
#[test]
fn mixed_case_native_tags_preserve_ordinary_bytes_and_stream_structurally() {
let source =
r#"<html><HEAD data-theme="dark"><title>T</title></HEAD><BODY>x</BODY></html>"#;
let mut parser = HtmlParser::new();
parser.parse("index.html", source).expect("parse fixture");
let protocol = Protocol::new(WebUIProtocol::new(parser.into_fragment_records()));
let mut ordinary = TestWriter::new();
WebUIHandler::new()
.render(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut ordinary,
)
.unwrap();
assert_eq!(ordinary.get_content(), source);
let mut streaming = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut streaming,
)
.unwrap();
assert!(streaming.output.contains(r#"<HEAD data-theme="dark">"#));
assert!(streaming.output.contains("</HEAD><BODY>x"));
let opening = streaming.output.find("<HEAD").expect("mixed-case head");
let marker = streaming.output.find(STREAMING_MARKER).expect("marker");
let title = streaming.output.find("<title>").expect("title");
assert!(opening < marker && marker < title);
assert_eq!(streaming.output.matches("data-webui-boundary").count(), 1);
}
#[test]
fn unnamespaced_signal_is_ordinary_content() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
WebUIFragment::signal("body_end".to_string(), false),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
)]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let mut writer = TestWriter::new();
WebUIHandler::new()
.render(
&protocol,
&test_json!({ "body_end": "content" }),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.expect("current protocol must render");
assert!(writer.get_content().contains("content"));
}
#[test]
fn authored_raw_signal_keys_remain_content_in_both_render_modes() {
let source = concat!(
"<html><head></head><body>",
"{{{head_start}}}|{{{head_end}}}|{{{body_start}}}|{{{body_end}}}|",
"{{{boundary_start:0}}}|{{{boundary_end:0}}}|{{{streaming_root:forged}}}",
"</body></html>",
);
let state = test_json!({
"head_start": "hs",
"head_end": "he",
"body_start": "bs",
"body_end": "be",
"boundary_start:0": "b0s",
"boundary_end:0": "b0e",
"streaming_root:forged": "root",
});
let mut parser = HtmlParser::new();
parser.parse("index.html", source).expect("parse fixture");
let protocol = Protocol::new(WebUIProtocol::new(parser.into_fragment_records()));
let expected = "<html><head></head><body>hs|he|bs|be|b0s|b0e|root</body></html>";
let mut ordinary = TestWriter::new();
WebUIHandler::new()
.render(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut ordinary,
)
.unwrap();
assert_eq!(ordinary.get_content(), expected);
let mut streaming = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut streaming,
)
.unwrap();
assert!(streaming.output.contains("<body>hs|he|bs|be|b0s|b0e|root"));
assert!(!streaming.output.contains("<!--wb:"));
assert_eq!(streaming.output.matches(STREAMING_MARKER).count(), 1);
assert_eq!(streaming.output.matches("data-webui-boundary").count(), 1);
}
#[test]
fn streaming_rejects_structural_signals_after_terminal() {
let fragments = HashMap::from([(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
structural_fragment("body_end"),
WebUIFragment::raw("</body>"),
structural_fragment("boundary_start:0"),
WebUIFragment::raw("</html>"),
],
contains_boundary: false,
},
)]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let mut streaming = FlushTestWriter::default();
let result = WebUIHandler::new().render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut streaming,
);
match result {
Err(HandlerError::StreamingBoundary(error)) => {
assert_eq!(error.signal, "boundary_start:0");
assert!(
error
.reason
.contains("structural signal arrived after body_end"),
"reason: {}",
error.reason
);
}
other => panic!("expected post-terminal rejection, got {other:?}"),
}
assert_eq!(
streaming.output,
concat!(
"<html><head><meta name=\"webui-streaming\" content=\"1\"></head><body>",
"<script type=\"application/json\" data-webui-boundary>[0,4,0,{}]</script>",
"<webui-hydrate></webui-hydrate></body>"
)
);
assert!(!streaming.output.contains("<!--wb:0-->"));
let mut ordinary = TestWriter::new();
WebUIHandler::new()
.render(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut ordinary,
)
.unwrap();
assert_eq!(
ordinary.get_content(),
"<html><head></head><body></body></html>"
);
}
fn disjoint_streaming_protocol_ext(
hosts: &[&str],
emit_root_signal: bool,
include_styles: bool,
) -> Protocol {
let mut fragments = HashMap::new();
let mut entry = vec![
WebUIFragment::raw("<!DOCTYPE html><html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
];
for (sequence, host) in hosts.iter().enumerate() {
entry.push(WebUIFragment::boundary(
sequence as u32,
"index.html",
format!("boundary-{sequence}"),
None,
));
entry.push(WebUIFragment::raw(format!("<{host}")));
if emit_root_signal {
entry.push(structural_fragment(format!("streaming_root:{host}")));
}
entry.push(WebUIFragment::raw(">"));
entry.push(WebUIFragment::component(*host));
entry.push(WebUIFragment::raw(format!("</{host}>")));
entry.push(WebUIFragment::boundary_end(sequence as u32));
}
entry.push(structural_fragment("body_end"));
entry.push(WebUIFragment::raw("</body></html>"));
fragments.insert(
"index.html".to_string(),
FragmentList {
fragments: entry,
contains_boundary: true,
},
);
fragments.insert(
"comp-a".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<b>A</b>")],
contains_boundary: false,
},
);
fragments.insert(
"comp-b".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<b>B</b>")],
contains_boundary: false,
},
);
let mut document = WebUIProtocol::new(fragments);
document.initial_state_strategy = InitialStateStrategy::Components as i32;
document.components.insert(
"comp-a".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<i>A</i>","th":1}"#.to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["a_count".to_string()],
..Default::default()
},
);
document.components.insert(
"comp-b".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<i>B</i>","th":1}"#.to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["b_count".to_string()],
..Default::default()
},
);
if include_styles {
document.set_css_strategy(webui_protocol::CssStrategy::Style);
document
.components
.get_mut("comp-a")
.expect("comp-a component")
.css = ".comp-a{color:red}".to_string();
document
.components
.get_mut("comp-b")
.expect("comp-b component")
.css = ".comp-b{color:blue}".to_string();
document.populate_style_closures(&["index.html"]);
}
Protocol::new(document)
}
fn disjoint_streaming_protocol(hosts: &[&str]) -> Protocol {
disjoint_streaming_protocol_ext(hosts, true, false)
}
fn styled_disjoint_streaming_protocol(hosts: &[&str]) -> Protocol {
disjoint_streaming_protocol_ext(hosts, true, true)
}
fn streaming_root_validation_protocol(
mut host_fragments: Vec<WebUIFragment>,
tail: Vec<WebUIFragment>,
) -> Protocol {
let mut entry = vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::boundary(0, "index.html", "boundary-0", None),
];
entry.append(&mut host_fragments);
entry.extend(tail);
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: entry,
contains_boundary: true,
},
),
(
"comp-a".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<b>A</b>")],
contains_boundary: false,
},
),
(
"comp-b".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<b>B</b>")],
contains_boundary: false,
},
),
]);
Protocol::new(WebUIProtocol::new(fragments))
}
fn completed_streaming_tail() -> Vec<WebUIFragment> {
vec![
WebUIFragment::raw("</comp-a>"),
WebUIFragment::boundary_end(0),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
]
}
fn assert_streaming_root_error(
protocol: &Protocol,
expected_signal: &str,
expected_reason: &str,
) {
let mut writer = FlushTestWriter::default();
let result = WebUIHandler::new().render_streaming(
protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
);
match result {
Err(HandlerError::StreamingBoundary(error)) => {
assert_eq!(error.signal, expected_signal);
assert!(
error.reason.contains(expected_reason),
"reason: {}",
error.reason
);
}
other => panic!("expected streaming-root rejection, got {other:?}"),
}
}
#[test]
fn streaming_root_signal_injects_data_ws_inside_boundary() {
let protocol = disjoint_streaming_protocol(&["comp-a"]);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&test_json!({ "a_count": 1 }),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert!(
writer.output.contains("<comp-a data-ws>"),
"streamed host must carry data-ws: {}",
writer.output
);
}
#[test]
fn streaming_root_signal_preserves_ordinary_output_bytes() {
let with_signal = disjoint_streaming_protocol_ext(&["comp-a", "comp-b"], true, false);
let without_signal = disjoint_streaming_protocol_ext(&["comp-a", "comp-b"], false, false);
let state = test_json!({ "a_count": 1, "b_count": 2 });
let plugin = || {
WebUIHandler::with_plugin(
|| Box::new(crate::plugin::webui::WebUIHydrationPlugin::new()),
)
};
let mut with_writer = TestWriter::new();
let mut without_writer = TestWriter::new();
plugin()
.render(
&with_signal,
&state,
&RenderOptions::new("index.html", "/"),
&mut with_writer,
)
.unwrap();
plugin()
.render(
&without_signal,
&state,
&RenderOptions::new("index.html", "/"),
&mut without_writer,
)
.unwrap();
let dom_prefix = |content: &str| -> String {
content
.split_once(r#"<script type="application/json" id="webui-data""#)
.map_or_else(|| content.to_string(), |(head, _)| head.to_string())
};
assert_eq!(
dom_prefix(&with_writer.get_content()),
dom_prefix(&without_writer.get_content())
);
assert!(!with_writer.get_content().contains("data-ws"));
assert!(with_writer.get_content().contains("<comp-a>"));
}
#[test]
fn streaming_component_without_root_signal_is_rejected() {
let protocol = streaming_root_validation_protocol(
vec![
WebUIFragment::raw("<comp-a>"),
WebUIFragment::component("comp-a"),
],
completed_streaming_tail(),
);
assert_streaming_root_error(
&protocol,
"comp-a",
"has no compiler-owned streaming_root signal",
);
}
#[test]
fn duplicate_and_misplaced_streaming_root_signals_are_rejected() {
let duplicate = streaming_root_validation_protocol(
vec![
WebUIFragment::raw("<comp-a"),
structural_fragment("streaming_root:comp-a"),
structural_fragment("streaming_root:comp-a"),
WebUIFragment::raw(">"),
WebUIFragment::component("comp-a"),
],
completed_streaming_tail(),
);
assert_streaming_root_error(&duplicate, "comp-a", "is misplaced");
let after_tag_close = streaming_root_validation_protocol(
vec![
WebUIFragment::raw("<comp-a>"),
structural_fragment("streaming_root:comp-a"),
WebUIFragment::component("comp-a"),
],
completed_streaming_tail(),
);
assert_streaming_root_error(&after_tag_close, "comp-a", "is misplaced");
let detached_from_opening = streaming_root_validation_protocol(
vec![
WebUIFragment::raw("not an opening tag"),
structural_fragment("streaming_root:comp-a"),
WebUIFragment::raw(">"),
WebUIFragment::component("comp-a"),
],
completed_streaming_tail(),
);
assert_streaming_root_error(
&detached_from_opening,
"comp-a",
"unclosed component opening-tag close",
);
}
#[test]
fn mismatched_streaming_root_signal_is_rejected() {
let protocol = streaming_root_validation_protocol(
vec![
WebUIFragment::raw("<comp-a"),
structural_fragment("streaming_root:comp-b"),
WebUIFragment::raw(">"),
WebUIFragment::component("comp-a"),
],
completed_streaming_tail(),
);
assert_streaming_root_error(&protocol, "comp-b", "root signal names <comp-b>");
}
#[test]
fn pending_streaming_root_is_rejected_at_structural_ends() {
let protocol = streaming_root_validation_protocol(
vec![
WebUIFragment::raw("<comp-a"),
structural_fragment("streaming_root:comp-a"),
WebUIFragment::raw(">"),
],
vec![structural_fragment("boundary_end:0")],
);
assert_streaming_root_error(&protocol, "comp-a", "is misplaced");
}
#[test]
fn streaming_root_outside_boundary_is_rejected() {
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::raw("<comp-a"),
structural_fragment("streaming_root:comp-a"),
WebUIFragment::raw(">"),
WebUIFragment::component("comp-a"),
WebUIFragment::raw("</comp-a>"),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
],
contains_boundary: false,
},
),
(
"comp-a".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<b>A</b>")],
contains_boundary: false,
},
),
]);
let protocol = Protocol::new(WebUIProtocol::new(fragments));
let mut writer = FlushTestWriter::default();
let result = WebUIHandler::new().render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
);
match result {
Err(HandlerError::StreamingBoundary(err)) => {
assert_eq!(err.signal, "comp-a");
assert!(
err.reason
.contains("no boundary or generated component span"),
"reason: {}",
err.reason
);
}
other => panic!("expected outside-boundary rejection, got {other:?}"),
}
}
fn parser_route_protocol(with_boundary: bool) -> Protocol {
let mut parser = HtmlParser::with_options(DomStrategy::Light);
parser
.component_registry_mut()
.register_component(ComponentRegistration::new(
"route-page",
"<p>route content</p>",
None,
true,
))
.expect("register route component");
let route = r#"<route path="/" component="route-page" exact />"#;
let html = if with_boundary {
format!(
"<html><head></head><body>\
<boundary name=\"route\">{route}</boundary>\
</body></html>"
)
} else {
format!("<html><head></head><body>{route}</body></html>")
};
parser
.parse("index.html", &html)
.expect("parse route streaming fixture");
let mut document = WebUIProtocol::new(parser.into_fragment_records());
document.components.insert(
"route-page".to_string(),
webui_protocol::ComponentData::default(),
);
Protocol::new(document)
}
#[test]
fn streaming_parser_route_host_is_marked_inside_explicit_boundary() {
let protocol = parser_route_protocol(true);
let mut writer = FlushTestWriter::default();
WebUIHandler::new()
.render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert!(
writer
.output
.contains("<route-page data-ws><p>route content</p></route-page>"),
"matched route host must be deferred before upgrade: {}",
writer.output
);
}
#[test]
fn streaming_parser_route_host_outside_boundary_is_rejected() {
let protocol = parser_route_protocol(false);
let mut writer = FlushTestWriter::default();
let result = WebUIHandler::new().render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/"),
&mut writer,
);
match result {
Err(HandlerError::StreamingBoundary(error)) => {
assert_eq!(error.signal, "route-page");
assert!(
error
.reason
.contains("no boundary or generated component span"),
"reason: {}",
error.reason
);
}
other => panic!("expected route boundary rejection, got {other:?}"),
}
assert!(
!writer.output.contains("<route-page>"),
"an unmarked custom-element opening must never be completed"
);
}
#[test]
fn streaming_checkpoint_preserves_relative_route_base_for_metadata() {
let entry = vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::boundary(0, "index.html", "boundary-0", None),
WebUIFragment::route_from(webui_protocol::WebUiFragmentRoute {
path: "/account".into(),
fragment_id: "account-shell".into(),
exact: false,
..Default::default()
}),
WebUIFragment::boundary_end(0),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
];
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: entry,
contains_boundary: true,
},
),
(
"account-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(
webui_protocol::WebUiFragmentRoute {
path: "./details".into(),
fragment_id: "details-page".into(),
exact: true,
pending_component: "details-loading".into(),
error_component: "details-error".into(),
..Default::default()
},
)],
contains_boundary: false,
},
),
(
"details-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>details</p>")],
contains_boundary: false,
},
),
(
"details-loading".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>loading</p>")],
contains_boundary: false,
},
),
(
"details-error".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>error</p>")],
contains_boundary: false,
},
),
]);
let mut document = WebUIProtocol::new(fragments);
document.initial_state_strategy = InitialStateStrategy::Components as i32;
for name in [
"account-shell",
"details-page",
"details-loading",
"details-error",
] {
document.components.insert(
name.to_string(),
webui_protocol::ComponentData {
template_json: format!(r#"{{"h":"<{name}></{name}>","th":1}}"#),
..Default::default()
},
);
}
let protocol = Protocol::new(document);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&test_json!({}),
&RenderOptions::new("index.html", "/account/details"),
&mut writer,
)
.unwrap();
let first = &writer.output[writer.flushes[0]..writer.flushes[1]];
assert!(
first.contains(r#""details-loading":"#),
"relative-route pending metadata missing: {first}"
);
assert!(
first.contains(r#""details-error":"#),
"relative-route error metadata missing: {first}"
);
}
#[test]
fn streaming_checkpoint_keeps_independent_siblings_beside_route_roots() {
let entry = vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::boundary(0, "index.html", "boundary-0", None),
WebUIFragment::raw("<static-shell"),
structural_fragment("streaming_root:static-shell"),
WebUIFragment::raw(">"),
WebUIFragment::component("static-shell"),
WebUIFragment::raw("</static-shell>"),
WebUIFragment::raw("<route-shell"),
structural_fragment("streaming_root:route-shell"),
WebUIFragment::raw(">"),
WebUIFragment::component("route-shell"),
WebUIFragment::raw("</route-shell>"),
WebUIFragment::boundary_end(0),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
];
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: entry,
contains_boundary: true,
},
),
(
"static-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::identifier("show_hidden"),
"static-hidden-if",
)],
contains_boundary: false,
},
),
(
"static-hidden-if".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("static-hidden")],
contains_boundary: false,
},
),
(
"static-hidden".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>hidden</p>")],
contains_boundary: false,
},
),
(
"route-shell".to_string(),
FragmentList {
fragments: vec![WebUIFragment::route_from(
webui_protocol::WebUiFragmentRoute {
path: "/account".into(),
fragment_id: "route-page".into(),
exact: true,
..Default::default()
},
)],
contains_boundary: false,
},
),
(
"route-page".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>route</p>")],
contains_boundary: false,
},
),
]);
let mut document = WebUIProtocol::new(fragments);
document.set_css_strategy(webui_protocol::CssStrategy::Link);
document.initial_state_strategy = InitialStateStrategy::Components as i32;
for (name, keys) in [
("static-shell", vec!["show_hidden", "static_count"]),
("static-hidden", vec!["hidden_count"]),
("route-shell", vec!["route_shell_count"]),
("route-page", vec!["route_count"]),
] {
document.components.insert(
name.to_string(),
webui_protocol::ComponentData {
template_json: format!(r#"{{"h":"<{name}></{name}>","th":1}}"#),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: keys.into_iter().map(str::to_string).collect(),
css_href: format!("/{name}.css"),
..Default::default()
},
);
}
document.populate_style_closures(&["index.html"]);
let protocol = Protocol::new(document);
assert_eq!(
protocol
.component_reachability()
.is_route_dependent(protocol.component_index()["route-shell"]),
Some(true),
"fixture must exercise request-aware checkpoint reachability"
);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&test_json!({
"show_hidden": false,
"static_count": 1,
"hidden_count": 2,
"route_shell_count": 3,
"route_count": 4,
"unrelated": "private",
}),
&RenderOptions::new("index.html", "/account"),
&mut writer,
)
.unwrap();
let checkpoint = &writer.output[writer.flushes[0]..writer.flushes[1]];
for template in ["static-shell", "static-hidden", "route-shell", "route-page"] {
assert!(
checkpoint.contains(&format!(r#""{template}":"#)),
"{template} metadata missing: {checkpoint}"
);
}
for state in [
r#""show_hidden":false"#,
r#""static_count":1"#,
r#""hidden_count":2"#,
r#""route_shell_count":3"#,
r#""route_count":4"#,
] {
assert!(
checkpoint.contains(state),
"{state} state missing: {checkpoint}"
);
}
assert!(
!checkpoint.contains("unrelated"),
"checkpoint leaked unrelated state: {checkpoint}"
);
assert!(
checkpoint.contains(
r#""css":["/static-shell.css","/static-hidden.css","/route-shell.css","/route-page.css"]"#
),
"checkpoint metadata lost source order: {checkpoint}"
);
assert!(
checkpoint.contains(r#""componentStyles":"#)
&& checkpoint.contains(r#""strategy":"link""#)
&& checkpoint.contains(r#""static-shell":["static-shell","static-hidden"]"#),
"checkpoint is missing tree-local style metadata: {checkpoint}"
);
for resource in ["static-shell", "static-hidden", "route-shell", "route-page"] {
assert_eq!(
writer
.output
.matches(&format!(r#"data-webui-resource="{resource}""#))
.count(),
1,
"Document delivery state must persist across streamed checkpoints"
);
}
}
#[test]
fn streaming_checkpoints_carry_boundary_local_templates_and_state() {
let protocol = disjoint_streaming_protocol(&["comp-a", "comp-b", "comp-a"]);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let state = test_json!({ "a_count": 1, "b_count": 2, "serverOnly": "secret" });
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&state,
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.flushes.len(),
5,
"bootstrap, three boundary commits, and one terminal"
);
let segment = |index: usize| -> &str {
&writer.output[writer.flushes[index]..writer.flushes[index + 1]]
};
let b0 = segment(0);
let b1 = segment(1);
let b2 = segment(2);
assert!(b0.contains(r#""templates":{"comp-a":"#), "b0: {b0}");
assert!(b0.contains(r#""a_count":1"#), "b0: {b0}");
assert!(
b0.contains(r#"[0,0,0,{"declarationId":0,"componentStyles":"#),
"b0: {b0}"
);
assert!(b0.contains(r#""inventory":"01""#), "b0: {b0}");
assert!(!b0.contains("comp-b"), "b0 leaked comp-b: {b0}");
assert!(!b0.contains("b_count"), "b0 leaked b_count: {b0}");
assert!(b1.contains(r#""templates":{"comp-b":"#), "b1: {b1}");
assert!(b1.contains(r#""b_count":2"#), "b1: {b1}");
assert!(
b1.contains(r#"[1,0,1,{"declarationId":1,"componentStyles":"#),
"b1: {b1}"
);
assert!(b1.contains(r#""inventory":"02""#), "b1: {b1}");
assert!(
!b1.contains(r#""templates":{"comp-a"#),
"b1 re-sent comp-a: {b1}"
);
assert!(!b1.contains("a_count"), "b1 leaked a_count: {b1}");
assert!(b2.contains(r#""a_count":1"#), "b2: {b2}");
assert!(
b2.contains(r#"[2,0,2,{"declarationId":2,"componentStyles":"#),
"b2: {b2}"
);
assert!(b2.contains(r#""inventory":"""#), "b2: {b2}");
assert!(
!b2.contains(r#""templates""#),
"b2 re-sent a template: {b2}"
);
assert!(!writer.output.contains("serverOnly"));
}
#[test]
fn streaming_checkpoints_emit_style_resources_and_closures_once() {
let protocol = styled_disjoint_streaming_protocol(&["comp-a", "comp-b", "comp-a"]);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&test_json!({ "a_count": 1, "b_count": 2 }),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let segment = |index: usize| -> &str {
&writer.output[writer.flushes[index]..writer.flushes[index + 1]]
};
let checkpoint_styles = |checkpoint: &str| -> Value {
let script = checkpoint
.rfind(r#"<script type="application/json" data-webui-boundary"#)
.unwrap();
let payload_start = script + checkpoint[script..].find('>').unwrap() + 1;
let payload_end =
payload_start + checkpoint[payload_start..].find("</script>").unwrap();
let record: Value =
serde_json::from_str(&checkpoint[payload_start..payload_end]).unwrap();
record[3]["componentStyles"].clone()
};
let first = checkpoint_styles(segment(0));
assert!(first["resources"]["comp-a"].is_object());
assert!(first["resources"]["comp-b"].is_object());
assert!(first["closures"]["index.html"].is_array());
assert!(first["closures"]["comp-a"].is_array());
let second = checkpoint_styles(segment(1));
assert_eq!(second["resources"], test_json!({}));
assert_eq!(second["closures"], test_json!({ "comp-b": ["comp-b"] }));
let repeated = checkpoint_styles(segment(2));
assert_eq!(repeated["resources"], test_json!({}));
assert_eq!(repeated["closures"], test_json!({}));
for resource in ["comp-a", "comp-b"] {
assert_eq!(
[&first, &second, &repeated]
.iter()
.filter(|styles| {
styles["resources"]
.as_object()
.is_some_and(|resources| resources.contains_key(resource))
})
.count(),
1,
"{resource} style metadata was serialized more than once"
);
}
}
#[test]
fn streaming_checkpoint_carries_reachable_unrendered_metadata_without_inventory() {
let entry = vec![
WebUIFragment::raw("<html><head>"),
structural_fragment("head_start"),
structural_fragment("head_end"),
WebUIFragment::raw("</head><body>"),
structural_fragment("body_start"),
WebUIFragment::boundary(0, "index.html", "boundary-0", None),
WebUIFragment::raw("<comp-a"),
structural_fragment("streaming_root:comp-a"),
WebUIFragment::raw(">"),
WebUIFragment::component("comp-a"),
WebUIFragment::raw("</comp-a>"),
WebUIFragment::boundary_end(0),
WebUIFragment::boundary(1, "index.html", "boundary-1", None),
WebUIFragment::raw("<comp-hidden"),
structural_fragment("streaming_root:comp-hidden"),
WebUIFragment::raw(">"),
WebUIFragment::component("comp-hidden"),
WebUIFragment::raw("</comp-hidden>"),
WebUIFragment::boundary_end(1),
structural_fragment("body_end"),
WebUIFragment::raw("</body></html>"),
];
let fragments = HashMap::from([
(
"index.html".to_string(),
FragmentList {
fragments: entry,
contains_boundary: true,
},
),
(
"comp-a".to_string(),
FragmentList {
fragments: vec![WebUIFragment::if_cond(
ConditionExpr::identifier("show_hidden"),
"hidden-if",
)],
contains_boundary: false,
},
),
(
"hidden-if".to_string(),
FragmentList {
fragments: vec![WebUIFragment::component("comp-hidden")],
contains_boundary: false,
},
),
(
"comp-hidden".to_string(),
FragmentList {
fragments: vec![WebUIFragment::raw("<p>hidden</p>")],
contains_boundary: false,
},
),
]);
let mut document = WebUIProtocol::new(fragments);
document.set_css_strategy(webui_protocol::CssStrategy::Module);
document.initial_state_strategy = InitialStateStrategy::Components as i32;
document.components.insert(
"comp-a".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<div></div>","th":1}"#.to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["show_hidden".to_string()],
..Default::default()
},
);
document.components.insert(
"comp-hidden".to_string(),
webui_protocol::ComponentData {
template_json: r#"{"h":"<p>hidden</p>","th":1}"#.to_string(),
css: ".hidden{display:block}".to_string(),
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["hidden_count".to_string()],
..Default::default()
},
);
document.populate_style_closures(&["index.html"]);
let protocol = Protocol::new(document);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&test_json!({
"show_hidden": false,
"hidden_count": 7,
"unrelated": "private",
}),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
let boundary = |index: usize| -> &str {
&writer.output[writer.flushes[index]..writer.flushes[index + 1]]
};
let first = boundary(0);
let second = boundary(1);
assert!(first.contains(r#""comp-a":"#), "first: {first}");
assert!(first.contains(r#""comp-hidden":"#), "first: {first}");
assert!(first.contains(r#""show_hidden":false"#), "first: {first}");
assert!(first.contains(r#""hidden_count":7"#), "first: {first}");
assert!(first.contains(r#""inventory":"01""#), "first: {first}");
assert!(
first.contains(r#"<script type="importmap""#),
"first: {first}"
);
assert!(
!first.contains("unrelated"),
"first leaked unrelated state: {first}"
);
assert!(
!second.contains(r#""templates""#),
"second re-sent metadata: {second}"
);
assert!(
!second.contains(r#"<script type="importmap""#),
"second re-sent CSS: {second}"
);
assert!(second.contains(r#""hidden_count":7"#), "second: {second}");
assert!(second.contains(r#""inventory":"02""#), "second: {second}");
assert_eq!(
writer
.output
.matches(r#"<script type="importmap">"#)
.count(),
1,
"import-map output: {}",
writer.output
);
}
#[test]
fn streaming_flush_count_is_boundaries_plus_one() {
for &count in &[1usize, 3, 10, 100] {
let hosts: Vec<&str> = (0..count)
.map(|index| if index % 2 == 0 { "comp-a" } else { "comp-b" })
.collect();
let protocol = disjoint_streaming_protocol(&hosts);
let handler = WebUIHandler::with_plugin(|| {
Box::new(crate::plugin::webui::WebUIHydrationPlugin::new())
});
let mut writer = FlushTestWriter::default();
handler
.render_streaming(
&protocol,
&test_json!({ "a_count": 1, "b_count": 2 }),
&RenderOptions::new("index.html", "/"),
&mut writer,
)
.unwrap();
assert_eq!(
writer.flushes.len(),
count + 2,
"expected bootstrap + boundaries + terminal flushes for {count} boundaries"
);
}
}
#[test]
fn checkpoint_state_key_scratch_reuses_allocation_and_stays_local() {
let mut fragments = HashMap::new();
fragments.insert("comp-a".to_string(), FragmentList::default());
fragments.insert("comp-b".to_string(), FragmentList::default());
let mut protocol = WebUIProtocol::new(fragments);
protocol.initial_state_strategy = InitialStateStrategy::Components as i32;
protocol.components.insert(
"comp-a".to_string(),
webui_protocol::ComponentData {
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["a".to_string(), "shared".to_string()],
..Default::default()
},
);
protocol.components.insert(
"comp-b".to_string(),
webui_protocol::ComponentData {
hydration_mode: StateProjectionMode::Keys as i32,
hydration_keys: vec!["b".to_string(), "shared".to_string()],
..Default::default()
},
);
let protocol = Protocol::new(protocol);
let component_index = protocol.component_index();
let reachability = protocol.component_reachability();
let mut scratch = Vec::with_capacity(INITIAL_KEY_CAPACITY);
assert!(!collect_hydration_key_ids_into(
protocol.protocol(),
reachability,
[component_index["comp-a"]],
&mut scratch,
));
let keys = scratch
.iter()
.filter_map(|id| reachability.hydration_key(*id))
.collect::<Vec<_>>();
assert_eq!(keys, ["a", "shared"]);
let pointer = scratch.as_ptr();
let capacity = scratch.capacity();
assert!(!collect_hydration_key_ids_into(
protocol.protocol(),
reachability,
[component_index["comp-b"]],
&mut scratch,
));
let keys = scratch
.iter()
.filter_map(|id| reachability.hydration_key(*id))
.collect::<Vec<_>>();
assert_eq!(keys, ["b", "shared"]);
assert_eq!(scratch.as_ptr(), pointer);
assert_eq!(scratch.capacity(), capacity);
}
}