use std::path::{Path, PathBuf}; #[test] fn canonical_examples_do_not_author_browser_javascript() { // req: examples/005 let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); let examples = root.join("examples"); let mut failures = Vec::new(); scan_examples(&examples, &mut |path, text| { if path.components().any(|part| part.as_os_str() == "tests") { return; } for (line_no, line) in text.lines().enumerate() { let trimmed = line.trim(); if trimmed.contains(">(); for token in [ "SafeHtml", "Effect", "Slot", "KeyedSlot", "ResourceId", "EffectBatch", "BuildFingerprint", "Atom", "ComponentRef", "EventName", "GeneratedTarget", "Handle", "ParamName", "page", ] { assert!( !exported.contains(&token), "hemx::prelude must not export low-level `{token}`" ); } assert!(prelude.contains("Html"), "hemx::prelude should export Html"); assert!( prelude.contains("IntoEffect"), "hemx::prelude should export IntoEffect" ); } #[test] fn canonical_example_docs_do_not_teach_low_level_plumbing() { // req: dx/002 req: dx/003 req: examples/003 let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); let examples = root.join("examples"); let forbidden = [ "generated handles and slots", "generated hemx handles", "generated handle ids", "numeric handle ids", "runtime lowering", "application/hemx responses", "EffectBatch", "manual registry", "HandlerRegistry", ".into_registry()", "InteractionForm", "register_handle(", "lower_html(", "render_html(", "SafeHtml", "KeyedSlot", "Slot<", "Effect::", "opcode", "wire format", "manual generated", "Effect::batch", "Effect::class", "Effect::move", "Effect::set", "Effect::broadcast", "Effect::ack", "SyncEffect::", "postcard DOM ops", "data-hid", "data-sid", ".render(&", "addEventListener(", "querySelector", "querySelectorAll", ]; let mut failures = Vec::new(); scan_examples(&examples, &mut |path, text| { if path.extension().and_then(|ext| ext.to_str()) != Some("md") || is_advanced_boundary_doc(text) { return; } for (line_no, line) in text.lines().enumerate() { if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) { failures.push(format!( "{}:{}: example docs must teach generated ergonomic APIs, not `{token}`", path.display(), line_no + 1 )); } } }); assert!(failures.is_empty(), "{}", failures.join("\n")); } #[test] fn example_docs_do_not_show_runtime_metadata_as_authoring_contract() { // req: dx/006 req: misc/006 let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); let examples = root.join("examples"); let forbidden = [ "data-hid", "data-sid", "data-aid", "data-fid", "[data-hid", "[data-sid", ]; let mut failures = Vec::new(); scan_examples(&examples, &mut |path, text| { if path.extension().and_then(|ext| ext.to_str()) != Some("md") { return; } for (line_no, line) in text.lines().enumerate() { if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) { failures.push(format!( "{}:{}: example docs must describe generated authoring APIs, not runtime metadata `{token}`", path.display(), line_no + 1 )); } } }); assert!(failures.is_empty(), "{}", failures.join("\n")); } #[test] fn browser_e2e_does_not_shortcut_product_interactions() { // req: examples/005 let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); let browser_e2e = root.join("examples/techdemo/tests/browser_e2e.rs"); let source = std::fs::read_to_string(browser_e2e).unwrap(); assert!( !source.contains("fetch(\"/\""), "browser E2E must drive UI, not post directly" ); assert!( !source.contains("applyBatch(buffer"), "browser E2E must not apply wire batches manually" ); } #[test] fn html_examples_slot_targets_dispatch_to_component_put() { // req: runtime/005 req: examples/001 req: htmx_equivalents/001 // Regression: slots named after a component (e.g. contact_card, editable_row) // must lower injected partials using that component's handle table, not the // containing page's handle table. let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); let template_dir = root.join("examples/html_examples/templates"); let out_dir = std::env::temp_dir().join(format!( "hemx-test-html-examples-slot-targets-{}", std::process::id() )); let _ = std::fs::remove_dir_all(&out_dir); std::fs::create_dir_all(&out_dir).unwrap(); let result = std::panic::catch_unwind(|| { hemx_build::app() .template_dir(&template_dir) .out_dir(&out_dir) .run() .expect("hemx-build should generate html_examples artifacts"); let generated = std::fs::read_to_string(out_dir.join("hemx.generated.rs")).unwrap_or_default(); for component in ["contact_card", "editable_row"] { let component_path = format!("super::super::{component}"); let impl_header = format!("impl SlotTarget"); let put_call = format!("{component_path}::put(self.slot, view)"); assert!( generated.contains(&impl_header), "gallery::targets should have a component-specific SlotTarget impl for `{component}`; missing `{impl_header}`" ); assert!( generated.contains(&put_call), "gallery::targets::{component} put/replace should dispatch to `{component_path}::put`; missing `{put_call}`" ); } }); let _ = std::fs::remove_dir_all(&out_dir); result.unwrap(); } fn scan_examples(dir: &Path, visit: &mut impl FnMut(&Path, &str)) { for entry in std::fs::read_dir(dir).unwrap() { let entry = entry.unwrap(); let path = entry.path(); if path.is_dir() { scan_examples(&path, visit); continue; } if !is_example_source(&path) { continue; } let text = std::fs::read_to_string(&path).unwrap(); visit(&path, &text); } } fn is_advanced_boundary_doc(text: &str) -> bool { text.contains("advanced/low-level north-star boundary sketch") } fn is_example_source(path: &PathBuf) -> bool { matches!( path.extension().and_then(|ext| ext.to_str()), Some("rs" | "heml" | "html" | "md") ) } fn allowed_example_script(path: &Path, line: &str) -> bool { // req: examples/005 line.contains(r#""#) || (path.ends_with("examples/techdemo/templates/app_shell.heml") && line.contains(r#""#)) || (path.ends_with("examples/saas/templates/app_shell.heml") && line.contains(r#""#)) } fn contains_inline_event_handler(line: &str) -> bool { let bytes = line.as_bytes(); let mut i = 0; while i + 3 < bytes.len() { let boundary = bytes[i].is_ascii_whitespace() || bytes[i] == b'<'; if boundary && bytes[i + 1] == b'o' && bytes[i + 2] == b'n' && bytes[i + 3].is_ascii_lowercase() { let mut j = i + 4; while j < bytes.len() && bytes[j].is_ascii_lowercase() { j += 1; } while j < bytes.len() && bytes[j].is_ascii_whitespace() { j += 1; } if j < bytes.len() && bytes[j] == b'=' { return true; } } i += 1; } false }