mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 20:23:09 +00:00
Fix playwright tests (#2695)
This commit is contained in:
parent
9479d2376d
commit
bd484842bd
28 changed files with 477 additions and 330 deletions
|
@ -117,6 +117,7 @@ impl BuildResult {
|
|||
&self,
|
||||
serve: &ServeArguments,
|
||||
fullstack_address: Option<SocketAddr>,
|
||||
workspace: &std::path::Path,
|
||||
) -> std::io::Result<Option<Child>> {
|
||||
if self.web {
|
||||
return Ok(None);
|
||||
|
@ -124,12 +125,8 @@ impl BuildResult {
|
|||
|
||||
let arguments = RuntimeCLIArguments::new(serve.address.address(), fullstack_address);
|
||||
let executable = self.executable.canonicalize()?;
|
||||
// This is the /dist folder generally
|
||||
let output_folder = executable.parent().unwrap();
|
||||
// This is the workspace folder
|
||||
let workspace_folder = output_folder.parent().unwrap();
|
||||
Ok(Some(
|
||||
Command::new(&executable)
|
||||
Command::new(executable)
|
||||
// When building the fullstack server, we need to forward the serve arguments (like port) to the fullstack server through env vars
|
||||
.env(
|
||||
dioxus_cli_config::__private::SERVE_ENV,
|
||||
|
@ -138,7 +135,7 @@ impl BuildResult {
|
|||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.current_dir(workspace_folder)
|
||||
.current_dir(workspace)
|
||||
.spawn()?,
|
||||
))
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ pub async fn serve_all(serve: Serve, dioxus_crate: DioxusCrate) -> Result<()> {
|
|||
|
||||
// If we have a build result, open it
|
||||
for build_result in results.iter() {
|
||||
let child = build_result.open(&serve.server_arguments, server.fullstack_address());
|
||||
let child = build_result.open(&serve.server_arguments, server.fullstack_address(), &dioxus_crate.workspace_dir());
|
||||
match child {
|
||||
Ok(Some(child_proc)) => builder.children.push((build_result.platform,child_proc)),
|
||||
Err(_e) => break,
|
||||
|
|
|
@ -21,6 +21,7 @@ use axum_server::tls_rustls::RustlsConfig;
|
|||
use dioxus_cli_config::{Platform, WebHttpsConfig};
|
||||
use dioxus_hot_reload::{DevserverMsg, HotReloadMsg};
|
||||
use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures_util::stream;
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use hyper::header::ACCEPT;
|
||||
use hyper::HeaderMap;
|
||||
|
@ -577,7 +578,9 @@ pub(crate) fn open_browser(base_path: Option<String>, address: SocketAddr, https
|
|||
}
|
||||
|
||||
fn get_available_port(address: IpAddr) -> Option<u16> {
|
||||
(8000..9000).find(|port| TcpListener::bind((address, *port)).is_ok())
|
||||
TcpListener::bind((address, 0))
|
||||
.map(|listener| listener.local_addr().unwrap().port())
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Middleware that intercepts html requests if the status is "Building" and returns a loading page instead
|
||||
|
@ -598,7 +601,12 @@ async fn build_status_middleware(
|
|||
let html = include_str!("../../assets/loading.html");
|
||||
return axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::from(html))
|
||||
// Load the html loader then keep loading forever
|
||||
// We never close the stream so any headless testing framework (like playwright) will wait until the real build is done
|
||||
.body(Body::from_stream(
|
||||
stream::once(async move { Ok::<_, std::convert::Infallible>(html) })
|
||||
.chain(stream::pending()),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,7 +276,6 @@ impl SuspenseBoundaryProps {
|
|||
to: Option<&mut M>,
|
||||
) -> usize {
|
||||
let mut scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]);
|
||||
|
||||
// If the ScopeId is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that
|
||||
if scope_id.is_placeholder() {
|
||||
{
|
||||
|
@ -300,78 +299,80 @@ impl SuspenseBoundaryProps {
|
|||
// Store the scope id for the next render
|
||||
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0;
|
||||
}
|
||||
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap();
|
||||
|
||||
let children = RenderReturn {
|
||||
node: props
|
||||
.children
|
||||
.as_ref()
|
||||
.map(|node| node.clone_mounted())
|
||||
.map_err(Clone::clone),
|
||||
};
|
||||
|
||||
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
children.create(dom, parent, None::<&mut M>);
|
||||
});
|
||||
|
||||
// Store the (now mounted) children back into the scope state
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
props.children = children.clone().node;
|
||||
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let suspense_context = scope_state
|
||||
.state()
|
||||
.suspense_location()
|
||||
.suspense_context()
|
||||
.unwrap()
|
||||
.clone();
|
||||
// If there are suspended futures, render the fallback
|
||||
let nodes_created = if !suspense_context.suspended_futures().is_empty() {
|
||||
let (node, nodes_created) =
|
||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||
&dom.runtime,
|
||||
scope_id,
|
||||
)
|
||||
.unwrap();
|
||||
suspense_context.set_suspended_nodes(children.into());
|
||||
let suspense_placeholder = props.fallback.call(suspense_context);
|
||||
let node = RenderReturn {
|
||||
node: suspense_placeholder,
|
||||
};
|
||||
let nodes_created = node.create(dom, parent, to);
|
||||
(node, nodes_created)
|
||||
});
|
||||
|
||||
dom.runtime.clone().with_scope_on_stack(scope_id, || {
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
scope_state.last_rendered_node = Some(node);
|
||||
|
||||
nodes_created
|
||||
} else {
|
||||
// Otherwise just render the children in the real dom
|
||||
debug_assert!(children.mount.get().mounted());
|
||||
let nodes_created = suspense_context
|
||||
.under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to));
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
scope_state.last_rendered_node = Some(children);
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
suspense_context.take_suspended_nodes();
|
||||
mark_suspense_resolved(dom, scope_id);
|
||||
|
||||
let children = RenderReturn {
|
||||
node: props
|
||||
.children
|
||||
.as_ref()
|
||||
.map(|node| node.clone_mounted())
|
||||
.map_err(Clone::clone),
|
||||
};
|
||||
|
||||
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
children.create(dom, parent, None::<&mut M>);
|
||||
});
|
||||
|
||||
// Store the (now mounted) children back into the scope state
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
props.children = children.clone().node;
|
||||
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let suspense_context = scope_state
|
||||
.state()
|
||||
.suspense_location()
|
||||
.suspense_context()
|
||||
.unwrap()
|
||||
.clone();
|
||||
// If there are suspended futures, render the fallback
|
||||
let nodes_created = if !suspense_context.suspended_futures().is_empty() {
|
||||
let (node, nodes_created) =
|
||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||
&dom.runtime,
|
||||
scope_id,
|
||||
)
|
||||
.unwrap();
|
||||
suspense_context.set_suspended_nodes(children.into());
|
||||
let suspense_placeholder = props.fallback.call(suspense_context);
|
||||
let node = RenderReturn {
|
||||
node: suspense_placeholder,
|
||||
};
|
||||
let nodes_created = node.create(dom, parent, to);
|
||||
(node, nodes_created)
|
||||
});
|
||||
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
scope_state.last_rendered_node = Some(node);
|
||||
|
||||
nodes_created
|
||||
} else {
|
||||
// Otherwise just render the children in the real dom
|
||||
debug_assert!(children.mount.get().mounted());
|
||||
let nodes_created = suspense_context
|
||||
.under_suspense_boundary(&dom.runtime(), || children.create(dom, parent, to));
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
scope_state.last_rendered_node = Some(children);
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
suspense_context.take_suspended_nodes();
|
||||
mark_suspense_resolved(dom, scope_id);
|
||||
|
||||
nodes_created
|
||||
};
|
||||
nodes_created
|
||||
};
|
||||
|
||||
nodes_created
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
@ -385,65 +386,69 @@ impl SuspenseBoundaryProps {
|
|||
only_write_templates: impl FnOnce(&mut M),
|
||||
replace_with: usize,
|
||||
) {
|
||||
let _runtime = RuntimeGuard::new(dom.runtime());
|
||||
let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else {
|
||||
return;
|
||||
};
|
||||
dom.runtime.clone().with_scope_on_stack(scope_id, || {
|
||||
let _runtime = RuntimeGuard::new(dom.runtime());
|
||||
let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Reset the suspense context
|
||||
let suspense_context = scope_state
|
||||
.state()
|
||||
.suspense_location()
|
||||
.suspense_context()
|
||||
.unwrap()
|
||||
.clone();
|
||||
suspense_context.inner.suspended_tasks.borrow_mut().clear();
|
||||
// Reset the suspense context
|
||||
let suspense_context = scope_state
|
||||
.state()
|
||||
.suspense_location()
|
||||
.suspense_context()
|
||||
.unwrap()
|
||||
.clone();
|
||||
suspense_context.inner.suspended_tasks.borrow_mut().clear();
|
||||
|
||||
// Get the parent of the suspense boundary to later create children with the right parent
|
||||
let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone();
|
||||
let mount = currently_rendered.mount.get();
|
||||
let parent = dom
|
||||
.mounts
|
||||
.get(mount.0)
|
||||
.expect("suspense placeholder is not mounted")
|
||||
.parent;
|
||||
// Get the parent of the suspense boundary to later create children with the right parent
|
||||
let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone();
|
||||
let mount = currently_rendered.mount.get();
|
||||
let parent = dom
|
||||
.mounts
|
||||
.get(mount.0)
|
||||
.expect("suspense placeholder is not mounted")
|
||||
.parent;
|
||||
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
|
||||
// Unmount any children to reset any scopes under this suspense boundary
|
||||
let children = props
|
||||
.children
|
||||
.as_ref()
|
||||
.map(|node| node.clone_mounted())
|
||||
.map_err(Clone::clone);
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap();
|
||||
let suspended = suspense_context.suspended_nodes();
|
||||
if let Some(node) = suspended {
|
||||
node.remove_node(&mut *dom, None::<&mut M>, None);
|
||||
}
|
||||
// Replace the rendered nodes with resolved nodes
|
||||
currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with));
|
||||
// Unmount any children to reset any scopes under this suspense boundary
|
||||
let children = props
|
||||
.children
|
||||
.as_ref()
|
||||
.map(|node| node.clone_mounted())
|
||||
.map_err(Clone::clone);
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
let suspended = suspense_context.suspended_nodes();
|
||||
if let Some(node) = suspended {
|
||||
node.remove_node(&mut *dom, None::<&mut M>, None);
|
||||
}
|
||||
// Replace the rendered nodes with resolved nodes
|
||||
currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with));
|
||||
|
||||
// Switch to only writing templates
|
||||
only_write_templates(to);
|
||||
// Switch to only writing templates
|
||||
only_write_templates(to);
|
||||
|
||||
let children = RenderReturn { node: children };
|
||||
children.mount.take();
|
||||
let children = RenderReturn { node: children };
|
||||
children.mount.take();
|
||||
|
||||
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
children.create(dom, parent, Some(to));
|
||||
});
|
||||
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
children.create(dom, parent, Some(to));
|
||||
});
|
||||
|
||||
// Store the (now mounted) children back into the scope state
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
props.children = children.clone().node;
|
||||
scope_state.last_rendered_node = Some(children);
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap();
|
||||
suspense_context.take_suspended_nodes();
|
||||
// Store the (now mounted) children back into the scope state
|
||||
let scope_state = &mut dom.scopes[scope_id.0];
|
||||
let props = Self::downcast_from_props(&mut *scope_state.props).unwrap();
|
||||
props.children = children.clone().node;
|
||||
scope_state.last_rendered_node = Some(children);
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
suspense_context.take_suspended_nodes();
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn diff<M: WriteMutations>(
|
||||
|
@ -451,127 +456,140 @@ impl SuspenseBoundaryProps {
|
|||
dom: &mut VirtualDom,
|
||||
to: Option<&mut M>,
|
||||
) {
|
||||
let scope = &mut dom.scopes[scope_id.0];
|
||||
let myself = Self::downcast_from_props(&mut *scope.props)
|
||||
.unwrap()
|
||||
.clone();
|
||||
dom.runtime.clone().with_scope_on_stack(scope_id, || {
|
||||
let scope = &mut dom.scopes[scope_id.0];
|
||||
let myself = Self::downcast_from_props(&mut *scope.props)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone_mounted();
|
||||
let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone_mounted();
|
||||
|
||||
let Self {
|
||||
fallback, children, ..
|
||||
} = myself;
|
||||
let Self {
|
||||
fallback, children, ..
|
||||
} = myself;
|
||||
|
||||
let suspense_context = scope.state().suspense_boundary().unwrap().clone();
|
||||
let suspended_nodes = suspense_context.suspended_nodes();
|
||||
let suspended = !suspense_context.suspended_futures().is_empty();
|
||||
match (suspended_nodes, suspended) {
|
||||
// We already have suspended nodes that still need to be suspended
|
||||
// Just diff the normal and suspended nodes
|
||||
(Some(suspended_nodes), true) => {
|
||||
let new_suspended_nodes: VNode = RenderReturn { node: children }.into();
|
||||
let suspense_context = scope.state().suspense_boundary().unwrap().clone();
|
||||
let suspended_nodes = suspense_context.suspended_nodes();
|
||||
let suspended = !suspense_context.suspended_futures().is_empty();
|
||||
match (suspended_nodes, suspended) {
|
||||
// We already have suspended nodes that still need to be suspended
|
||||
// Just diff the normal and suspended nodes
|
||||
(Some(suspended_nodes), true) => {
|
||||
let new_suspended_nodes: VNode = RenderReturn { node: children }.into();
|
||||
|
||||
// Diff the placeholder nodes in the dom
|
||||
let new_placeholder =
|
||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||
let old_placeholder = last_rendered_node;
|
||||
let new_placeholder = RenderReturn {
|
||||
node: fallback.call(suspense_context.clone()),
|
||||
};
|
||||
// Diff the placeholder nodes in the dom
|
||||
let new_placeholder =
|
||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||
let old_placeholder = last_rendered_node;
|
||||
let new_placeholder = RenderReturn {
|
||||
node: fallback.call(suspense_context.clone()),
|
||||
};
|
||||
|
||||
old_placeholder.diff_node(&new_placeholder, dom, to);
|
||||
new_placeholder
|
||||
old_placeholder.diff_node(&new_placeholder, dom, to);
|
||||
new_placeholder
|
||||
});
|
||||
|
||||
// Set the last rendered node to the placeholder
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
|
||||
|
||||
// Diff the suspended nodes in the background
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>);
|
||||
});
|
||||
|
||||
// Set the last rendered node to the placeholder
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
|
||||
let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||
&dom.runtime,
|
||||
scope_id,
|
||||
)
|
||||
.unwrap();
|
||||
suspense_context.set_suspended_nodes(new_suspended_nodes);
|
||||
}
|
||||
// We have no suspended nodes, and we are not suspended. Just diff the children like normal
|
||||
(None, false) => {
|
||||
let old_children = last_rendered_node;
|
||||
let new_children = RenderReturn { node: children };
|
||||
|
||||
// Diff the suspended nodes in the background
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>);
|
||||
});
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
old_children.diff_node(&new_children, dom, to);
|
||||
});
|
||||
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
suspense_context.set_suspended_nodes(new_suspended_nodes);
|
||||
}
|
||||
// We have no suspended nodes, and we are not suspended. Just diff the children like normal
|
||||
(None, false) => {
|
||||
let old_children = last_rendered_node;
|
||||
let new_children = RenderReturn { node: children };
|
||||
// Set the last rendered node to the new children
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
||||
}
|
||||
// We have no suspended nodes, but we just became suspended. Move the children to the background
|
||||
(None, true) => {
|
||||
let old_children = last_rendered_node;
|
||||
let new_children: VNode = RenderReturn { node: children }.into();
|
||||
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
old_children.diff_node(&new_children, dom, to);
|
||||
});
|
||||
let new_placeholder = RenderReturn {
|
||||
node: fallback.call(suspense_context.clone()),
|
||||
};
|
||||
|
||||
// Set the last rendered node to the new children
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
||||
}
|
||||
// We have no suspended nodes, but we just became suspended. Move the children to the background
|
||||
(None, true) => {
|
||||
let old_children = last_rendered_node;
|
||||
let new_children: VNode = RenderReturn { node: children }.into();
|
||||
|
||||
let new_placeholder = RenderReturn {
|
||||
node: fallback.call(suspense_context.clone()),
|
||||
};
|
||||
|
||||
// Move the children to the background
|
||||
let mount = old_children.mount.get();
|
||||
let mount = dom.mounts.get(mount.0).expect("mount should exist");
|
||||
let parent = mount.parent;
|
||||
|
||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||
old_children.move_node_to_background(
|
||||
std::slice::from_ref(&*new_placeholder),
|
||||
parent,
|
||||
dom,
|
||||
to,
|
||||
);
|
||||
});
|
||||
|
||||
// Then diff the new children in the background
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
old_children.diff_node(&new_children, dom, None::<&mut M>);
|
||||
});
|
||||
|
||||
// Set the last rendered node to the new suspense placeholder
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
|
||||
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
suspense_context.set_suspended_nodes(new_children);
|
||||
|
||||
un_resolve_suspense(dom, scope_id);
|
||||
} // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground
|
||||
(Some(old_suspended_nodes), false) => {
|
||||
let old_placeholder = last_rendered_node;
|
||||
let new_children = RenderReturn { node: children };
|
||||
|
||||
// First diff the two children nodes in the background
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>);
|
||||
|
||||
// Then replace the placeholder with the new children
|
||||
let mount = old_placeholder.mount.get();
|
||||
// Move the children to the background
|
||||
let mount = old_children.mount.get();
|
||||
let mount = dom.mounts.get(mount.0).expect("mount should exist");
|
||||
let parent = mount.parent;
|
||||
old_placeholder.replace(std::slice::from_ref(&*new_children), parent, dom, to);
|
||||
});
|
||||
|
||||
// Set the last rendered node to the new children
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
||||
suspense_context.in_suspense_placeholder(&dom.runtime(), || {
|
||||
old_children.move_node_to_background(
|
||||
std::slice::from_ref(&*new_placeholder),
|
||||
parent,
|
||||
dom,
|
||||
to,
|
||||
);
|
||||
});
|
||||
|
||||
let suspense_context =
|
||||
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
|
||||
.unwrap();
|
||||
suspense_context.take_suspended_nodes();
|
||||
// Then diff the new children in the background
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
old_children.diff_node(&new_children, dom, None::<&mut M>);
|
||||
});
|
||||
|
||||
mark_suspense_resolved(dom, scope_id);
|
||||
// Set the last rendered node to the new suspense placeholder
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
|
||||
|
||||
let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||
&dom.runtime,
|
||||
scope_id,
|
||||
)
|
||||
.unwrap();
|
||||
suspense_context.set_suspended_nodes(new_children);
|
||||
|
||||
un_resolve_suspense(dom, scope_id);
|
||||
} // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground
|
||||
(Some(old_suspended_nodes), false) => {
|
||||
let old_placeholder = last_rendered_node;
|
||||
let new_children = RenderReturn { node: children };
|
||||
|
||||
// First diff the two children nodes in the background
|
||||
suspense_context.under_suspense_boundary(&dom.runtime(), || {
|
||||
old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>);
|
||||
|
||||
// Then replace the placeholder with the new children
|
||||
let mount = old_placeholder.mount.get();
|
||||
let mount = dom.mounts.get(mount.0).expect("mount should exist");
|
||||
let parent = mount.parent;
|
||||
old_placeholder.replace(
|
||||
std::slice::from_ref(&*new_children),
|
||||
parent,
|
||||
dom,
|
||||
to,
|
||||
);
|
||||
});
|
||||
|
||||
// Set the last rendered node to the new children
|
||||
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
||||
|
||||
let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
|
||||
&dom.runtime,
|
||||
scope_id,
|
||||
)
|
||||
.unwrap();
|
||||
suspense_context.take_suspended_nodes();
|
||||
|
||||
mark_suspense_resolved(dom, scope_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,10 @@ impl Document for DesktopDocument {
|
|||
fn set_title(&self, title: String) {
|
||||
self.desktop_ctx.window.set_title(&title);
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a desktop-target's JavaScript evaluator.
|
||||
|
|
|
@ -104,7 +104,7 @@ server = [
|
|||
"dep:parking_lot",
|
||||
"dioxus-interpreter-js",
|
||||
"dep:clap",
|
||||
"dioxus-cli-config/read-from-args"
|
||||
"dioxus-cli-config/read-from-args",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
//! This module contains the document providers for the fullstack platform.
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) mod server;
|
||||
#[cfg(feature = "web")]
|
||||
#[cfg(feature = "server")]
|
||||
pub use server::ServerDocument;
|
||||
#[cfg(all(feature = "web", feature = "document"))]
|
||||
pub(crate) mod web;
|
||||
|
|
|
@ -7,6 +7,10 @@ use std::cell::RefCell;
|
|||
use dioxus_lib::{html::document::*, prelude::*};
|
||||
use dioxus_ssr::Renderer;
|
||||
use generational_box::GenerationalBox;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
static RENDERER: Lazy<RwLock<Renderer>> = Lazy::new(|| RwLock::new(Renderer::new()));
|
||||
|
||||
#[derive(Default)]
|
||||
struct ServerDocumentInner {
|
||||
|
@ -19,35 +23,27 @@ struct ServerDocumentInner {
|
|||
|
||||
/// A Document provider that collects all contents injected into the head for SSR rendering.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ServerDocument(RefCell<ServerDocumentInner>);
|
||||
pub struct ServerDocument(RefCell<ServerDocumentInner>);
|
||||
|
||||
impl ServerDocument {
|
||||
pub(crate) fn render(
|
||||
&self,
|
||||
to: &mut impl std::fmt::Write,
|
||||
renderer: &mut Renderer,
|
||||
) -> std::fmt::Result {
|
||||
fn lazy_app(props: Element) -> Element {
|
||||
props
|
||||
}
|
||||
pub(crate) fn title(&self) -> Option<String> {
|
||||
let myself = self.0.borrow();
|
||||
myself.title.as_ref().map(|title| {
|
||||
RENDERER
|
||||
.write()
|
||||
.render_element(rsx! { title { "{title}" } })
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render(&self, to: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
let myself = self.0.borrow();
|
||||
let element = rsx! {
|
||||
if let Some(title) = myself.title.as_ref() {
|
||||
title { title: "{title}" }
|
||||
}
|
||||
{myself.meta.iter().map(|m| rsx! { {m} })}
|
||||
{myself.link.iter().map(|l| rsx! { {l} })}
|
||||
{myself.script.iter().map(|s| rsx! { {s} })}
|
||||
};
|
||||
|
||||
let mut dom = VirtualDom::new_with_props(lazy_app, element);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
// We don't hydrate the head, so we can set the pre_render flag to false to save a few bytes
|
||||
let was_pre_rendering = renderer.pre_render;
|
||||
renderer.pre_render = false;
|
||||
renderer.render_to(to, &dom)?;
|
||||
renderer.pre_render = was_pre_rendering;
|
||||
RENDERER.write().render_element_to(to, element)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -65,8 +61,12 @@ impl ServerDocument {
|
|||
/// Write the head element into the serialized context for hydration
|
||||
/// We write true if the head element was written to the DOM during server side rendering
|
||||
pub(crate) fn serialize_for_hydration(&self) {
|
||||
let serialize = crate::html_storage::serialize_context();
|
||||
serialize.push(&!self.0.borrow().streaming);
|
||||
// We only serialize the head elements if the web document feature is enabled
|
||||
#[cfg(feature = "document")]
|
||||
{
|
||||
let serialize = crate::html_storage::serialize_context();
|
||||
serialize.push(&!self.0.borrow().streaming);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,4 +137,8 @@ impl Document for ServerDocument {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,4 +55,8 @@ impl Document for FullstackWebDocument {
|
|||
}
|
||||
WebDocument.create_link(props);
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,8 +62,9 @@ pub fn launch(
|
|||
#[cfg(feature = "document")]
|
||||
let factory = move || {
|
||||
let mut vdom = factory();
|
||||
vdom.provide_root_context(std::rc::Rc::new(crate::document::web::FullstackWebDocument)
|
||||
as std::rc::Rc<dyn dioxus_lib::prelude::document::Document>);
|
||||
let document = std::rc::Rc::new(crate::document::web::FullstackWebDocument)
|
||||
as std::rc::Rc<dyn dioxus_lib::prelude::document::Document>;
|
||||
vdom.provide_root_context(document);
|
||||
vdom
|
||||
};
|
||||
|
||||
|
|
|
@ -18,8 +18,7 @@ pub mod launch;
|
|||
|
||||
pub use config::*;
|
||||
|
||||
#[cfg(feature = "document")]
|
||||
mod document;
|
||||
pub mod document;
|
||||
#[cfg(feature = "server")]
|
||||
mod render;
|
||||
#[cfg(feature = "server")]
|
||||
|
|
|
@ -7,9 +7,9 @@ use dioxus_ssr::{
|
|||
};
|
||||
use futures_channel::mpsc::Sender;
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::{collections::HashMap, future::Future};
|
||||
use std::{fmt::Write, sync::Arc};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
@ -160,9 +160,7 @@ impl SsrRendererPool {
|
|||
|
||||
let join_handle = spawn_platform(move || async move {
|
||||
let mut virtual_dom = virtual_dom_factory();
|
||||
#[cfg(feature = "document")]
|
||||
let document = std::rc::Rc::new(crate::document::server::ServerDocument::default());
|
||||
#[cfg(feature = "document")]
|
||||
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
|
||||
|
||||
// poll the future, which may call server_context()
|
||||
|
@ -171,34 +169,11 @@ impl SsrRendererPool {
|
|||
|
||||
let mut pre_body = String::new();
|
||||
|
||||
if let Err(err) = wrapper.render_head(&mut pre_body) {
|
||||
if let Err(err) = wrapper.render_head(&mut pre_body, &virtual_dom) {
|
||||
_ = into.start_send(Err(err));
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "document")]
|
||||
{
|
||||
// Collect any head content from the document provider and inject that into the head
|
||||
if let Err(err) = document.render(&mut pre_body, &mut renderer) {
|
||||
_ = into.start_send(Err(err.into()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable a warning when inserting contents into the head during streaming
|
||||
document.start_streaming();
|
||||
}
|
||||
|
||||
if let Err(err) = wrapper.render_before_body(&mut pre_body) {
|
||||
_ = into.start_send(Err(err));
|
||||
return;
|
||||
}
|
||||
if let Err(err) = write!(&mut pre_body, "<script>{INITIALIZE_STREAMING_JS}</script>") {
|
||||
_ = into.start_send(Err(
|
||||
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = Arc::new(StreamingRenderer::new(pre_body, into));
|
||||
let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
|
@ -237,15 +212,8 @@ impl SsrRendererPool {
|
|||
// Render the initial frame with loading placeholders
|
||||
let mut initial_frame = renderer.render(&virtual_dom);
|
||||
|
||||
// Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
|
||||
// Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
|
||||
let resolved_data = serialize_server_data(&virtual_dom, ScopeId::ROOT);
|
||||
initial_frame.push_str(&format!(
|
||||
r#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
|
||||
));
|
||||
|
||||
// Along with the initial frame, we render the html after the main element, but before the body tag closes. This should include the script that starts loading the wasm bundle.
|
||||
if let Err(err) = wrapper.render_after_main(&mut initial_frame) {
|
||||
if let Err(err) = wrapper.render_after_main(&mut initial_frame, &virtual_dom) {
|
||||
throw_error!(err);
|
||||
}
|
||||
stream.render(initial_frame);
|
||||
|
@ -311,7 +279,7 @@ impl SsrRendererPool {
|
|||
// If incremental rendering is enabled, add the new render to the cache without the streaming bits
|
||||
if let Some(incremental) = &self.incremental_cache {
|
||||
let mut cached_render = String::new();
|
||||
if let Err(err) = wrapper.render_before_body(&mut cached_render) {
|
||||
if let Err(err) = wrapper.render_head(&mut cached_render, &virtual_dom) {
|
||||
throw_error!(err);
|
||||
}
|
||||
cached_render.push_str(&post_streaming);
|
||||
|
@ -401,16 +369,48 @@ impl FullstackHTMLTemplate {
|
|||
pub fn render_head<R: std::fmt::Write>(
|
||||
&self,
|
||||
to: &mut R,
|
||||
virtual_dom: &VirtualDom,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
let ServeConfig { index, .. } = &self.cfg;
|
||||
|
||||
to.write_str(&index.head)?;
|
||||
let title = {
|
||||
let document: Option<std::rc::Rc<dyn dioxus_lib::prelude::document::Document>> =
|
||||
virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
|
||||
let document: Option<&crate::document::server::ServerDocument> = document
|
||||
.as_ref()
|
||||
.and_then(|document| document.as_any().downcast_ref());
|
||||
// Collect any head content from the document provider and inject that into the head
|
||||
document.and_then(|document| document.title())
|
||||
};
|
||||
|
||||
to.write_str(&index.head_before_title)?;
|
||||
if let Some(title) = title {
|
||||
to.write_str(&title)?;
|
||||
} else {
|
||||
to.write_str(&index.title)?;
|
||||
}
|
||||
to.write_str(&index.head_after_title)?;
|
||||
|
||||
let document: Option<std::rc::Rc<dyn dioxus_lib::prelude::document::Document>> =
|
||||
virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context());
|
||||
let document: Option<&crate::document::server::ServerDocument> = document
|
||||
.as_ref()
|
||||
.and_then(|document| document.as_any().downcast_ref());
|
||||
if let Some(document) = document {
|
||||
// Collect any head content from the document provider and inject that into the head
|
||||
document.render(to)?;
|
||||
|
||||
// Enable a warning when inserting contents into the head during streaming
|
||||
document.start_streaming();
|
||||
}
|
||||
|
||||
self.render_before_body(to)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render any content before the body of the page.
|
||||
pub fn render_before_body<R: std::fmt::Write>(
|
||||
fn render_before_body<R: std::fmt::Write>(
|
||||
&self,
|
||||
to: &mut R,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
|
@ -418,6 +418,8 @@ impl FullstackHTMLTemplate {
|
|||
|
||||
to.write_str(&index.close_head)?;
|
||||
|
||||
write!(to, "<script>{INITIALIZE_STREAMING_JS}</script>")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -425,9 +427,17 @@ impl FullstackHTMLTemplate {
|
|||
pub fn render_after_main<R: std::fmt::Write>(
|
||||
&self,
|
||||
to: &mut R,
|
||||
virtual_dom: &VirtualDom,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
let ServeConfig { index, .. } = &self.cfg;
|
||||
|
||||
// Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
|
||||
// Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
|
||||
let resolved_data = serialize_server_data(virtual_dom, ScopeId::ROOT);
|
||||
write!(
|
||||
to,
|
||||
r#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
|
||||
)?;
|
||||
to.write_str(&index.post_main)?;
|
||||
|
||||
Ok(())
|
||||
|
@ -444,6 +454,21 @@ impl FullstackHTMLTemplate {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wrap a body in the template
|
||||
pub fn wrap_body<R: std::fmt::Write>(
|
||||
&self,
|
||||
to: &mut R,
|
||||
virtual_dom: &VirtualDom,
|
||||
body: impl std::fmt::Display,
|
||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||
self.render_head(to, virtual_dom)?;
|
||||
write!(to, "{body}")?;
|
||||
self.render_after_main(to, virtual_dom)?;
|
||||
self.render_after_body(to)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn pre_renderer() -> Renderer {
|
||||
|
|
|
@ -118,8 +118,23 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
|
|||
panic!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
|
||||
});
|
||||
|
||||
// Strip out the head if it exists
|
||||
let mut head_before_title = String::new();
|
||||
let mut head_after_title = head;
|
||||
let mut title = String::new();
|
||||
if let Some((new_head_before_title, new_title)) = head_after_title.split_once("<title>") {
|
||||
let (new_title, new_head_after_title) = new_title
|
||||
.split_once("</title>")
|
||||
.expect("Failed to find closing </title> tag after <title> in index.html.");
|
||||
title = format!("<title>{new_title}</title>");
|
||||
head_before_title = new_head_before_title.to_string();
|
||||
head_after_title = new_head_after_title.to_string();
|
||||
}
|
||||
|
||||
IndexHtml {
|
||||
head,
|
||||
head_before_title,
|
||||
head_after_title,
|
||||
title,
|
||||
close_head,
|
||||
post_main: post_main.to_string(),
|
||||
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
|
||||
|
@ -128,7 +143,9 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct IndexHtml {
|
||||
pub(crate) head: String,
|
||||
pub(crate) head_before_title: String,
|
||||
pub(crate) head_after_title: String,
|
||||
pub(crate) title: String,
|
||||
pub(crate) close_head: String,
|
||||
pub(crate) post_main: String,
|
||||
pub(crate) after_closing_body_tag: String,
|
||||
|
|
|
@ -51,29 +51,36 @@ pub trait Document {
|
|||
self.new_evaluator(format!("document.title = {title:?};"));
|
||||
}
|
||||
|
||||
/// Create a new meta tag
|
||||
fn create_meta(&self, props: MetaProps) {
|
||||
let attributes = props.attributes();
|
||||
let js = create_element_in_head("meta", &attributes, None);
|
||||
self.new_evaluator(js);
|
||||
}
|
||||
|
||||
/// Create a new script tag
|
||||
fn create_script(&self, props: ScriptProps) {
|
||||
let attributes = props.attributes();
|
||||
let js = create_element_in_head("script", &attributes, props.script_contents());
|
||||
self.new_evaluator(js);
|
||||
}
|
||||
|
||||
/// Create a new style tag
|
||||
fn create_style(&self, props: StyleProps) {
|
||||
let attributes = props.attributes();
|
||||
let js = create_element_in_head("style", &attributes, props.style_contents());
|
||||
self.new_evaluator(js);
|
||||
}
|
||||
|
||||
/// Create a new link tag
|
||||
fn create_link(&self, props: head::LinkProps) {
|
||||
let attributes = props.attributes();
|
||||
let js = create_element_in_head("link", &attributes, None);
|
||||
self.new_evaluator(js);
|
||||
}
|
||||
|
||||
/// Get a reference to the document as `dyn Any`
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
/// The default No-Op document
|
||||
|
@ -84,6 +91,10 @@ impl Document for NoOpDocument {
|
|||
tracing::error!("Eval is not supported on this platform. If you are using dioxus fullstack, you can wrap your code with `client! {{}}` to only include the code that runs eval in the client bundle.");
|
||||
UnsyncStorage::owner().insert(Box::new(NoOpEvaluator))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct NoOpEvaluator;
|
||||
|
|
|
@ -21,6 +21,10 @@ impl Document for LiveviewDocument {
|
|||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
||||
LiveviewEvaluator::create(self.query.clone(), js)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a liveview-target's JavaScript evaluator.
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
// @ts-check
|
||||
const { test, expect } = require("@playwright/test");
|
||||
|
||||
test("button click", async ({ page }) => {
|
||||
test("hydration", async ({ page }) => {
|
||||
await page.goto("http://localhost:3333");
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Expect the page to contain the pending text.
|
||||
const main = page.locator("#main");
|
||||
await expect(main).toContainText("Server said: ...");
|
||||
|
||||
// Expect the page to contain the counter text.
|
||||
const main = page.locator("#main");
|
||||
await expect(main).toContainText("hello axum! 12345");
|
||||
// Expect the title to contain the counter text.
|
||||
await expect(page).toHaveTitle("hello axum! 12345");
|
||||
|
@ -15,23 +17,14 @@ test("button click", async ({ page }) => {
|
|||
let button = page.locator("button.increment-button");
|
||||
await button.click();
|
||||
|
||||
// Click the server button.
|
||||
let serverButton = page.locator("button.server-button");
|
||||
await serverButton.click();
|
||||
|
||||
// Expect the page to contain the updated counter text.
|
||||
await expect(main).toContainText("hello axum! 12346");
|
||||
// Expect the title to contain the updated counter text.
|
||||
await expect(page).toHaveTitle("hello axum! 12346");
|
||||
});
|
||||
|
||||
test("fullstack communication", async ({ page }) => {
|
||||
await page.goto("http://localhost:3333");
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Expect the page to contain the counter text.
|
||||
const main = page.locator("#main");
|
||||
await expect(main).toContainText("Server said: ...");
|
||||
|
||||
// Click the increment button.
|
||||
let button = page.locator("button.server-button");
|
||||
await button.click();
|
||||
|
||||
// Expect the page to contain the updated counter text.
|
||||
await expect(main).toContainText("Server said: Hello from the server!");
|
||||
|
|
|
@ -4,9 +4,23 @@ const { test, expect } = require("@playwright/test");
|
|||
test.use({ javaScriptEnabled: false });
|
||||
|
||||
test("text appears in the body without javascript", async ({ page }) => {
|
||||
await page.goto("http://localhost:5050", { waitUntil: "commit" });
|
||||
// Wait for the page to finish building. Reload until it's ready
|
||||
for (let i = 0; i < 50; i++) {
|
||||
// If the page doesn't contain #building or "Backend connection failed", we're ready
|
||||
let building_count = await page.locator("#building").count();
|
||||
building_count += await page
|
||||
.locator("body", { hasText: "backend connection failed" })
|
||||
.count();
|
||||
if (building_count === 0) {
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(1000);
|
||||
await page.goto("http://localhost:5050", { waitUntil: "commit" });
|
||||
}
|
||||
// If we wait until the whole page loads, the content of the site should still be in the body even if javascript is disabled
|
||||
// It will not be visible, and may not be in the right order/location, but SEO should still work
|
||||
await page.goto("http://localhost:5050");
|
||||
await page.waitForLoadState("load");
|
||||
|
||||
const body = page.locator("body");
|
||||
const textExpected = [
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
// @ts-check
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { timeout } = require("./playwright.config");
|
||||
|
||||
test("nested suspense resolves", async ({ page }) => {
|
||||
// Wait for the dev server to reload
|
||||
await page.goto("http://localhost:5050");
|
||||
// Then wait for the page to start loading
|
||||
await page.goto("http://localhost:5050", { waitUntil: "commit" });
|
||||
|
||||
// On the client, we should see some loading text
|
||||
|
|
|
@ -30,8 +30,12 @@ module.exports = defineConfig({
|
|||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
// Increase the timeout for navigations to give dx time to build the project
|
||||
navigationTimeout: 50 * 60 * 1000,
|
||||
},
|
||||
|
||||
timeout: 50 * 60 * 1000,
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
|
|
|
@ -18,7 +18,7 @@ fn app() -> Element {
|
|||
rsx! {
|
||||
h1 { "hello axum! {count}" }
|
||||
button { class: "increment-button", onclick: move |_| count += 1, "Increment" }
|
||||
"Server said: {server_data:?}"
|
||||
"Server said: {server_data().unwrap():?}"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
const { test, expect } = require("@playwright/test");
|
||||
|
||||
test("suspense resolves on server", async ({ page }) => {
|
||||
// Wait for the dev server to reload
|
||||
await page.goto("http://localhost:4040");
|
||||
// Then wait for the page to start loading
|
||||
await page.goto("http://localhost:4040", { waitUntil: "commit" });
|
||||
|
||||
// On the client, we should see some loading text
|
||||
|
|
|
@ -558,6 +558,7 @@ fn merges_attributes() {
|
|||
/// - merging two expressions together
|
||||
/// - merging two literals together
|
||||
/// - merging a literal and an expression together
|
||||
///
|
||||
/// etc
|
||||
///
|
||||
/// We really only want to merge formatted things together
|
||||
|
|
|
@ -17,14 +17,7 @@ pub use crate::renderer::Renderer;
|
|||
///
|
||||
/// For advanced rendering, create a new `SsrRender`.
|
||||
pub fn render_element(element: Element) -> String {
|
||||
fn lazy_app(props: Element) -> Element {
|
||||
props
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new_with_props(lazy_app, element);
|
||||
dom.rebuild_in_place();
|
||||
|
||||
Renderer::new().render(&dom)
|
||||
Renderer::new().render_element(element)
|
||||
}
|
||||
|
||||
/// A convenience function to render an existing VirtualDom to a string
|
||||
|
|
|
@ -62,6 +62,27 @@ impl Renderer {
|
|||
self.render_scope(buf, dom, ScopeId::ROOT)
|
||||
}
|
||||
|
||||
/// Render an element to a string
|
||||
pub fn render_element(&mut self, element: Element) -> String {
|
||||
let mut buf = String::new();
|
||||
self.render_element_to(&mut buf, element).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
/// Render an element to the buffer
|
||||
pub fn render_element_to<W: Write + ?Sized>(
|
||||
&mut self,
|
||||
buf: &mut W,
|
||||
element: Element,
|
||||
) -> std::fmt::Result {
|
||||
fn lazy_app(props: Element) -> Element {
|
||||
props
|
||||
}
|
||||
let mut dom = VirtualDom::new_with_props(lazy_app, element);
|
||||
dom.rebuild_in_place();
|
||||
self.render_to(buf, &dom)
|
||||
}
|
||||
|
||||
/// Reset the renderer hydration state
|
||||
pub fn reset_hydration(&mut self) {
|
||||
self.dynamic_node_id = 0;
|
||||
|
|
|
@ -42,11 +42,22 @@ pub fn launch(
|
|||
|
||||
// Serve the program if we are running with cargo
|
||||
if std::env::var_os("CARGO").is_some() || std::env::var_os("DIOXUS_ACTIVE").is_some() {
|
||||
// Get the address the server should run on. If the CLI is running, the CLI proxies static generation into the main address
|
||||
// and we use the generated address the CLI gives us
|
||||
let cli_args = dioxus_cli_config::RuntimeCLIArguments::from_cli();
|
||||
let address = cli_args
|
||||
.as_ref()
|
||||
.map(|args| args.fullstack_address().address())
|
||||
.unwrap_or_else(|| std::net::SocketAddr::from(([127, 0, 0, 1], 8080)));
|
||||
|
||||
// Point the user to the CLI address if the CLI is running or the fullstack address if not
|
||||
let serve_address = cli_args
|
||||
.map(|args| args.cli_address())
|
||||
.unwrap_or_else(|| address);
|
||||
println!(
|
||||
"Serving static files from {} at http://127.0.0.1:8080",
|
||||
"Serving static files from {} at http://{serve_address}",
|
||||
path.display()
|
||||
);
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
|
||||
let mut serve_dir =
|
||||
ServeDir::new(path.clone()).call_fallback_on_method_not_allowed(true);
|
||||
|
@ -66,7 +77,7 @@ pub fn launch(
|
|||
})))
|
||||
};
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
||||
axum::serve(listener, router.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -111,6 +111,8 @@ async fn prerender_route(
|
|||
let context = server_context_for_route(&route);
|
||||
let wrapper = config.fullstack_template();
|
||||
let mut virtual_dom = VirtualDom::new(app);
|
||||
let document = std::rc::Rc::new(dioxus_fullstack::document::ServerDocument::default());
|
||||
virtual_dom.provide_root_context(document.clone() as std::rc::Rc<dyn Document>);
|
||||
with_server_context(context.clone(), || {
|
||||
tokio::task::block_in_place(|| virtual_dom.rebuild_in_place());
|
||||
});
|
||||
|
@ -119,10 +121,11 @@ async fn prerender_route(
|
|||
let mut wrapped = String::new();
|
||||
|
||||
// Render everything before the body
|
||||
wrapper.render_before_body(&mut wrapped)?;
|
||||
wrapper.render_head(&mut wrapped, &virtual_dom)?;
|
||||
|
||||
renderer.render_to(&mut wrapped, &virtual_dom)?;
|
||||
|
||||
wrapper.render_after_main(&mut wrapped, &virtual_dom)?;
|
||||
wrapper.render_after_body(&mut wrapped)?;
|
||||
|
||||
cache.cache(route, wrapped)
|
||||
|
|
|
@ -25,6 +25,10 @@ impl Document for WebDocument {
|
|||
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
||||
WebEvaluator::create(js)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Required to avoid blocking the Rust WASM thread.
|
||||
|
|
|
@ -30,5 +30,5 @@ pub fn launch_virtual_dom(vdom: VirtualDom, platform_config: Config) {
|
|||
|
||||
/// Launch the web application with the given root component and config
|
||||
pub fn launch_cfg(root: fn() -> Element, platform_config: Config) {
|
||||
launch_virtual_dom(VirtualDom::new(root), platform_config)
|
||||
launch(root, Vec::new(), platform_config);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue