#![cfg(debug_assertions)]
#![allow(deprecated)]
use forma_ir::parser::test_helpers::{
build_minimal_ir, encode_close_tag, encode_open_tag, encode_text,
};
use forma_server::assets::try_load_manifest;
use forma_server::{load_ir_modules, AssetManifest, PageHandle, RenderCtx};
#[derive(forma_ir::PageProps)]
struct Empty {}
use rust_embed::Embed;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tracing::field::{Field, Visit};
use tracing::span::{Attributes, Id, Record};
use tracing::{Event, Metadata, Subscriber};
#[derive(Embed)]
#[folder = "tests/fixtures/dev-reload/"]
struct DevDist;
fn dist() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/dev-reload")
}
fn publish(name: &str, bytes: &[u8]) {
let staged = dist().join(format!(".{name}.staged"));
std::fs::write(&staged, bytes).expect("write");
std::fs::rename(&staged, dist().join(name)).expect("rename");
}
fn page(text: &str) -> Vec<u8> {
let mut ops = encode_open_tag(0, &[]);
ops.extend_from_slice(&encode_open_tag(1, &[]));
ops.extend_from_slice(&encode_text(2));
ops.extend_from_slice(&encode_close_tag(1));
ops.extend_from_slice(&encode_close_tag(0));
build_minimal_ir(&["main", "p", text], &[], &ops, &[])
}
const DEV_MANIFEST: &str = r#"{"version":2,"writer":"3.0.0","app":"ledger","build":"dev","aliases":{"app.js":"app.js","page.ir":"page.ir"},"assets":{"app.js":{"immutable":false,"integrity":"sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"},"page.ir":{"immutable":false,"integrity":"sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"}},"routes":{"/":{"js":["app.js"],"css":[],"fonts":[],"total_size_br":0,"budget_warn_threshold":0,"ir":"page.ir","mode":"ssr"}}}"#;
fn serve(_manifest: &AssetManifest, page: &PageHandle<Empty>) -> String {
page.render(&Empty {}, &RenderCtx::new())
.expect("renders")
.into_parts_unchecked()
.html
}
#[derive(Clone, Default)]
struct Warnings(Arc<Mutex<Vec<(String, String)>>>);
impl Subscriber for Warnings {
fn enabled(&self, _: &Metadata<'_>) -> bool {
true
}
fn new_span(&self, _: &Attributes<'_>) -> Id {
Id::from_u64(1)
}
fn record(&self, _: &Id, _: &Record<'_>) {}
fn record_follows_from(&self, _: &Id, _: &Id) {}
fn event(&self, event: &Event<'_>) {
#[derive(Default)]
struct Fields(String, String);
impl Visit for Fields {
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "code" {
self.0 = value.to_string();
}
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.1 = format!("{value:?}");
}
}
}
if *event.metadata().level() == tracing::Level::WARN {
let mut fields = Fields::default();
event.record(&mut fields);
self.0.lock().expect("lock").push((fields.0, fields.1));
}
}
fn enter(&self, _: &Id) {}
fn exit(&self, _: &Id) {}
}
struct Cleanup;
impl Drop for Cleanup {
fn drop(&mut self) {
for name in [
"manifest.json",
"page.ir",
".manifest.json.staged",
".page.ir.staged",
] {
let _ = std::fs::remove_file(dist().join(name));
}
}
}
#[test]
fn an_edited_page_is_served_on_the_next_request_and_a_broken_edit_keeps_the_previous_page() {
let _cleanup = Cleanup;
publish("manifest.json", DEV_MANIFEST.as_bytes());
publish("page.ir", &page("first"));
let manifest = try_load_manifest::<DevDist>().expect("manifest");
let pages = load_ir_modules::<DevDist>(&manifest).expect("loads");
let modules = pages.bind::<Empty>("/").unwrap();
pages.finalize().unwrap();
assert!(serve(&manifest, &modules).contains("<main><p>first</p></main>"));
let warnings = Warnings::default();
tracing::subscriber::with_default(warnings.clone(), || {
publish("page.ir", &page("second"));
let html = serve(&manifest, &modules);
assert!(html.contains("<main><p>second</p></main>"), "{html}");
assert!(!html.contains("first"));
let good = page("third");
publish("page.ir", &good[..good.len() / 2]);
for _ in 0..3 {
assert!(serve(&manifest, &modules).contains("<main><p>second</p></main>"));
}
let logged = warnings.0.lock().expect("lock").clone();
assert_eq!(logged.len(), 1, "{logged:?}");
assert_eq!(logged[0].0, "LOAD-104");
assert!(
logged[0].1.contains("route /: IR file page.ir: LOAD-104"),
"{logged:?}"
);
publish("page.ir", &good);
assert!(serve(&manifest, &modules).contains("<main><p>third</p></main>"));
});
}
const _: () = {
fn shared<T: Send + Sync + 'static>() {}
let _ = shared::<PageHandle<Empty>>;
};