Fix playwright tests (#2695)

This commit is contained in:
Evan Almloff 2024-07-24 21:48:30 +02:00 committed by GitHub
parent 9479d2376d
commit bd484842bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 477 additions and 330 deletions

View file

@ -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()?,
))
}

View file

@ -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,

View file

@ -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();
}
}

View file

@ -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,11 +299,12 @@ impl SuspenseBoundaryProps {
// Store the scope id for the next render
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0;
}
dom.runtime.clone().with_scope_on_stack(scope_id, || {
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();
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
.unwrap();
let children = RenderReturn {
node: props
@ -337,7 +337,8 @@ impl SuspenseBoundaryProps {
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(
let suspense_context =
SuspenseContext::downcast_suspense_boundary_from_scope(
&dom.runtime,
scope_id,
)
@ -370,8 +371,8 @@ impl SuspenseBoundaryProps {
nodes_created
};
nodes_created
})
}
#[doc(hidden)]
@ -385,6 +386,7 @@ impl SuspenseBoundaryProps {
only_write_templates: impl FnOnce(&mut M),
replace_with: usize,
) {
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;
@ -417,7 +419,8 @@ impl SuspenseBoundaryProps {
.map(|node| node.clone_mounted())
.map_err(Clone::clone);
let suspense_context =
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id).unwrap();
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);
@ -442,8 +445,10 @@ impl SuspenseBoundaryProps {
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();
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
.unwrap();
suspense_context.take_suspended_nodes();
})
}
pub(crate) fn diff<M: WriteMutations>(
@ -451,6 +456,7 @@ impl SuspenseBoundaryProps {
dom: &mut VirtualDom,
to: Option<&mut M>,
) {
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()
@ -491,8 +497,10 @@ impl SuspenseBoundaryProps {
suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>);
});
let suspense_context =
SuspenseContext::downcast_suspense_boundary_from_scope(&dom.runtime, scope_id)
let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
&dom.runtime,
scope_id,
)
.unwrap();
suspense_context.set_suspended_nodes(new_suspended_nodes);
}
@ -539,8 +547,10 @@ impl SuspenseBoundaryProps {
// 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)
let suspense_context = SuspenseContext::downcast_suspense_boundary_from_scope(
&dom.runtime,
scope_id,
)
.unwrap();
suspense_context.set_suspended_nodes(new_children);
@ -558,20 +568,28 @@ impl SuspenseBoundaryProps {
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);
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)
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);
}
}
})
}
}

View file

@ -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.

View file

@ -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]

View file

@ -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;

View file

@ -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,10 +61,14 @@ 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) {
// 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);
}
}
}
impl Document for ServerDocument {
fn new_evaluator(&self, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
@ -137,4 +137,8 @@ impl Document for ServerDocument {
}
})
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}

View file

@ -55,4 +55,8 @@ impl Document for FullstackWebDocument {
}
WebDocument.create_link(props);
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}

View file

@ -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
};

View file

@ -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")]

View file

@ -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 {

View file

@ -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,

View file

@ -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;

View file

@ -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.

View file

@ -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!");

View file

@ -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 = [

View file

@ -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

View file

@ -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: [
{

View file

@ -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():?}"
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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();

View file

@ -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)

View file

@ -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.

View file

@ -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);
}