#[path = "../support/mod.rs"]
mod support;
use std::fs;
use serde_json::json;
use support::Sandbox;
#[test]
fn realtime_scaffold_sets_up_the_channel_once() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("live", &[]);
let (report, ok) = sandbox.json(&["g", "scaffold", "Post", "title:string", "--realtime"], &root);
assert!(ok, "{report}");
let created = report["created"].as_array().unwrap();
assert!(
created.contains(&json!("templates/posts/_row.html")) && created.contains(&json!("src/realtime.rs")),
"{report}"
);
assert_eq!(report["updated"], json!(["src/lib.rs", "Cargo.toml", "cloudflare.config.ts", "templates/layout.html"]));
assert_eq!(report["next"][3], "open http://localhost:8787/posts in a second window, then create a post");
let cargo = fs::read_to_string(root.join("Cargo.toml")).unwrap();
assert!(cargo.contains("features = [\"realtime\"] }"), "{cargo}");
let config = fs::read_to_string(root.join("cloudflare.config.ts")).unwrap();
assert!(
config.contains("CHANNELS: bindings.durableObject({ worker: \"live\", exportName: \"OcreChannel\" }),\n"),
"{config}"
);
assert!(config.contains("OcreChannel: exports.durableObject({ storage: \"sqlite\" }),\n"), "{config}");
let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap();
assert!(lib.contains("mod realtime;") && lib.contains(".merge(realtime::routes())"), "{lib}");
let module = fs::read_to_string(root.join("src/realtime.rs")).unwrap();
assert!(module.contains("Generated by `ocre g scaffold Post title:string --realtime`."), "{module}");
assert!(
module.contains(" // ocre:channels\n \"posts\" => {}\n _ => return Err(Error::NotFound),")
);
let controller = fs::read_to_string(root.join("src/posts.rs")).unwrap();
assert!(
controller
.contains("broadcast(&ctx, &realtime::prepend(\"posts\", &row(&record)?)).await;\n session")
);
assert!(controller.contains("broadcast(&ctx, &row(&record)?).await;"), "{controller}");
assert!(controller.contains("broadcast(&ctx, &realtime::remove(&format!(\"post_{id}\"))).await;"));
assert!(controller.contains("struct RowView<'a> {\n post: &'a Post,\n}"), "{controller}");
let index = fs::read_to_string(root.join("templates/posts/index.html")).unwrap();
assert!(index.contains("<div hx-ext=\"ws\" ws-connect=\"/realtime/posts\">\n<table>"), "{index}");
assert!(!index.contains("<script"), "the layout loads the extension: {index}");
let layout = fs::read_to_string(root.join("templates/layout.html")).unwrap();
assert!(
layout.contains("crossorigin=\"anonymous\"></script>\n <script src=\"https://unpkg.com/htmx-ext-ws@2.0.4/dist/ws.js\" crossorigin=\"anonymous\"></script>\n"),
"{layout}"
);
assert!(index.contains("<tbody id=\"posts\">\n {% for post in posts %}\n {% include \"posts/_row.html\" %}"));
assert!(index.contains("</table>\n</div>\n\n<nav class=\"pagination\">"), "{index}");
let row = fs::read_to_string(root.join("templates/posts/_row.html")).unwrap();
assert!(row.starts_with("<tr id=\"post_{{ post.id }}\"><td>{{ post.title }}</td>"), "{row}");
let (routes, ok) = sandbox.json(&["routes", "realtime"], &root);
assert!(ok, "{routes}");
assert_eq!(routes["routes"][0]["path"], "/realtime/{channel}", "{routes}");
let (report, ok) = sandbox.json(&["g", "scaffold", "Comment", "body:text", "--realtime"], &root);
assert!(ok, "{report}");
assert_eq!(
report["updated"],
json!(["src/models/mod.rs", "tests/factories/mod.rs", "src/lib.rs", "src/realtime.rs"])
);
let module = fs::read_to_string(root.join("src/realtime.rs")).unwrap();
assert!(module.contains("// ocre:channels\n \"comments\" => {}\n \"posts\" => {}\n"), "{module}");
fs::write(root.join("src/realtime.rs"), module.replace("\"comments\" => {}", "\"tags\" => {}")).unwrap();
let (report, ok) = sandbox.json(&["g", "scaffold", "Tag", "name:string", "--realtime"], &root);
assert!(ok, "{report}");
assert_eq!(report["updated"], json!(["src/models/mod.rs", "tests/factories/mod.rs", "src/lib.rs"]));
let module = fs::read_to_string(root.join("src/realtime.rs")).unwrap();
assert_eq!(module.matches("\"tags\" => {}").count(), 1, "{module}");
let config = fs::read_to_string(root.join("cloudflare.config.ts")).unwrap();
assert_eq!(config.matches("bindings.durableObject(").count(), 1, "{config}");
assert_eq!(config.matches("exports.durableObject(").count(), 1, "{config}");
let cargo = fs::read_to_string(root.join("Cargo.toml")).unwrap();
assert_eq!(cargo.matches("\"realtime\"").count(), 1, "{cargo}");
let lib = fs::read_to_string(root.join("src/lib.rs")).unwrap();
assert_eq!(lib.matches("mod realtime;").count(), 1, "{lib}");
let layout = fs::read_to_string(root.join("templates/layout.html")).unwrap();
assert_eq!(layout.matches("htmx-ext-ws").count(), 1, "{layout}");
}
#[test]
fn realtime_scaffold_without_the_channels_marker_writes_nothing() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("live", &[]);
fs::write(root.join("src/realtime.rs"), "// hand-written\n").unwrap();
let (report, ok) = sandbox.json(&["g", "scaffold", "Post", "title:string", "--realtime"], &root);
assert!(!ok);
assert_eq!(report["error"], "src/realtime.rs is missing the `// ocre:channels` marker");
assert!(report["hint"].as_str().unwrap().contains("add `\"posts\" => {}` there yourself"), "{report}");
assert!(!root.join("src/posts.rs").exists(), "nothing written on failure");
assert!(!fs::read_to_string(root.join("Cargo.toml")).unwrap().contains("realtime"));
}
#[test]
fn realtime_scaffold_without_the_exports_marker_writes_nothing() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("live", &[]);
let config = fs::read_to_string(root.join("cloudflare.config.ts")).unwrap().replace("// ocre:exports", "");
fs::write(root.join("cloudflare.config.ts"), &config).unwrap();
let (report, ok) = sandbox.json(&["g", "scaffold", "Post", "title:string", "--realtime"], &root);
assert!(!ok);
assert_eq!(report["error"], "cloudflare.config.ts is missing the `// ocre:exports` marker");
assert!(report["hint"].as_str().unwrap().contains("inside `worker.exports: { ... }`"), "{report}");
assert_eq!(fs::read_to_string(root.join("cloudflare.config.ts")).unwrap(), config);
assert!(!root.join("src/posts.rs").exists() && !root.join("src/realtime.rs").exists(), "nothing written");
}
#[test]
fn realtime_scaffold_puts_the_extension_in_the_head_of_any_layout() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("live", &[]);
fs::write(
root.join("templates/layout.html"),
"<html><head>\n<script src=\"/htmx.js\"></script>\n</head><body></body></html>\n",
)
.unwrap();
let (report, ok) = sandbox.json(&["g", "scaffold", "Post", "title:string", "--realtime"], &root);
assert!(ok, "{report}");
let layout = fs::read_to_string(root.join("templates/layout.html")).unwrap();
assert!(layout.contains("<script src=\"/htmx.js\"></script>\n <script src=\"https://unpkg.com/htmx-ext-ws@2.0.4/dist/ws.js\" crossorigin=\"anonymous\"></script>\n</head>"), "{layout}");
let root = sandbox.new_app("bare", &[]);
fs::write(root.join("templates/layout.html"), "{% block content %}{% endblock %}\n").unwrap();
let (report, ok) = sandbox.json(&["g", "scaffold", "Post", "title:string", "--realtime"], &root);
assert!(!ok);
assert_eq!(report["error"], "templates/layout.html has no </head>");
assert!(report["hint"].as_str().unwrap().contains("htmx-ext-ws@2.0.4/dist/ws.js"), "{report}");
assert!(!root.join("src/posts.rs").exists() && !root.join("src/realtime.rs").exists(), "nothing written");
}
#[test]
fn realtime_scaffold_needs_html_pages() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("api", &["--api"]);
let (report, ok) = sandbox.json(&["g", "scaffold", "Post", "title:string", "--realtime"], &root);
assert!(!ok);
assert_eq!(report["error"], "--realtime updates HTML pages; this app is API-only");
assert!(report["hint"].as_str().unwrap().starts_with("run `ocre g scaffold` without --realtime"));
assert!(!root.join("src/posts_api.rs").exists());
}