Files
storybook/tests/validate_examples.rs

274 lines
8.3 KiB
Rust
Raw Normal View History

//! Example Validation Tests
//!
//! These tests validate that example storybooks load correctly and demonstrate
//! documented features. Unlike unit/integration tests that validate specific
//! functionality, these tests ensure that:
//! 1. Examples parse and resolve successfully
//! 2. Examples demonstrate features as documented
//! 3. Examples serve as reliable educational resources
//!
//! Cross-references:
//! - examples/baker-family/README.md
//! - DEVELOPER-GUIDE.md
use std::path::PathBuf;
use storybook::Project;
/// Helper to load an example project from the examples/ directory
fn load_example(name: &str) -> Project {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("examples");
path.push(name);
assert!(path.exists(), "Example '{}' not found at {:?}", name, path);
Project::load(&path).unwrap_or_else(|e| panic!("Failed to load example '{}': {:?}", name, e))
}
// ============================================================================
// Baker Family Example Tests
// ============================================================================
#[test]
fn test_baker_family_loads_successfully() {
let project = load_example("baker-family");
// Should have exactly 5 characters
let char_count = project.characters().count();
assert_eq!(char_count, 5, "Baker family should have 5 characters");
// Verify all characters exist
assert!(
project.find_character("Martha").is_some(),
"Martha should exist"
);
assert!(
project.find_character("Jane").is_some(),
"Jane should exist"
);
assert!(
project.find_character("Emma").is_some(),
"Emma should exist"
);
assert!(
project.find_character("Henry").is_some(),
"Henry should exist"
);
assert!(
project.find_character("Roland").is_some(),
"Roland should exist"
);
}
#[test]
fn test_baker_family_martha_has_all_template_fields() {
let project = load_example("baker-family");
let martha = project
.find_character("Martha")
.expect("Martha should exist");
// Fields from Person template (base)
assert!(
martha.fields.contains_key("age"),
"Should have age from Person"
);
assert!(
martha.fields.contains_key("energy"),
"Should have energy from Person"
);
assert!(
martha.fields.contains_key("mood"),
"Should have mood from Person"
);
// Fields from Worker template (extends Person)
assert!(
martha.fields.contains_key("occupation"),
"Should have occupation from Worker"
);
assert!(
martha.fields.contains_key("work_ethic"),
"Should have work_ethic from Worker"
);
// Fields from Baker template (extends Worker)
assert!(
martha.fields.contains_key("specialty"),
"Should have specialty from Baker"
);
assert!(
martha.fields.contains_key("baking_skill"),
"Should have baking_skill from Baker"
);
assert!(
martha.fields.contains_key("customer_relations"),
"Should have customer_relations from Baker"
);
}
#[test]
fn test_baker_family_field_values() {
let project = load_example("baker-family");
let martha = project.find_character("Martha").unwrap();
// Martha sets age: 34 (overriding Person template range 0..100)
if let Some(storybook::syntax::ast::Value::Number(age)) = martha.fields.get("age") {
assert_eq!(*age, 34, "Martha's age should be 34");
} else {
panic!("age should be a Number");
}
// Martha sets specialty: Sourdough (overriding Baker template default Bread)
if let Some(storybook::syntax::ast::Value::Identifier(parts)) = martha.fields.get("specialty") {
assert_eq!(
parts,
&vec!["Sourdough".to_string()],
"Martha's specialty should be Sourdough"
);
} else {
panic!("specialty should be an Identifier");
}
}
#[test]
fn test_baker_family_has_expected_behaviors() {
let project = load_example("baker-family");
let behaviors: Vec<_> = project.behaviors().collect();
let behavior_names: Vec<_> = behaviors.iter().map(|b| b.name.as_str()).collect();
// Core behaviors for the family
let expected = vec![
"BakingWork",
"PrepKitchen",
"BasicNeeds",
"SocialInteraction",
"BakingSkills",
"CustomerService",
"PlayBehavior",
"LearnBehavior",
];
for name in expected {
assert!(
behavior_names.contains(&name),
"Should have behavior '{}', got: {:?}",
name,
behavior_names
);
}
}
#[test]
fn test_baker_family_has_expected_schedules() {
let project = load_example("baker-family");
let schedules: Vec<_> = project.schedules().collect();
assert!(!schedules.is_empty(), "Should have schedules");
let schedule_names: Vec<_> = schedules.iter().map(|s| s.name.as_str()).collect();
// Should have work schedules
assert!(
schedule_names
.iter()
.any(|&n| n.contains("Schedule") || n.contains("Week")),
"Should have schedule definitions, got: {:?}",
schedule_names
);
}
#[test]
fn test_baker_family_multi_file_structure() {
// Baker family is organized across multiple files:
// - schema/core_types.sb, baking_types.sb, world_types.sb, social_types.sb
// - schema/templates.sb, life_arcs.sb
// - behaviors/baker_behaviors.sb, person_behaviors.sb, child_behaviors.sb
// - behaviors/competition_behaviors.sb, henry_behaviors.sb
// - schedules/base_schedule.sb, baker_schedule.sb, school_schedule.sb,
// retired_schedule.sb
// - relationships/family_relationships.sb, bakery_relationships.sb
// - characters/*.sb
let project = load_example("baker-family");
// If all declaration types are present, multi-file loading works
assert!(project.characters().count() > 0, "Should have characters");
assert!(project.behaviors().count() > 0, "Should have behaviors");
assert!(project.schedules().count() > 0, "Should have schedules");
}
// ============================================================================
// Generic Validation Tests
// ============================================================================
#[test]
fn test_baker_family_all_characters_have_names_and_fields() {
let project = load_example("baker-family");
for character in project.characters() {
assert!(!character.name.is_empty(), "Character should have a name");
assert!(
!character.fields.is_empty(),
"Character {} should have fields",
character.name
);
}
}
#[test]
fn test_baker_family_schedules_have_valid_blocks() {
let project = load_example("baker-family");
for schedule in project.schedules() {
if schedule.blocks.is_empty() {
continue; // Some schedules might be empty/templates
}
for block in &schedule.blocks {
// Time ranges should be valid: start < end (or midnight wrap)
let start_mins = block.start.hour as u32 * 60 + block.start.minute as u32;
let end_mins = block.end.hour as u32 * 60 + block.end.minute as u32;
assert!(
start_mins < end_mins || end_mins == 0,
"Block in {} should have valid time: {:02}:{:02}-{:02}:{:02}",
schedule.name,
block.start.hour,
block.start.minute,
block.end.hour,
block.end.minute
);
}
}
}
#[test]
fn test_baker_family_behaviors_have_valid_structure() {
let project = load_example("baker-family");
for behavior in project.behaviors() {
assert!(!behavior.name.is_empty(), "Behavior should have a name");
// Just verify root node exists
let _ = &behavior.root;
}
}
// ============================================================================
// Example Validation
// ============================================================================
#[test]
fn test_baker_family_example_loads_without_errors() {
// Test that the baker-family example loads successfully
let result = std::panic::catch_unwind(|| {
load_example("baker-family");
});
assert!(
result.is_ok(),
"Baker family example should load without panicking"
);
}