Added "What's New in v0.3" section covering species, concepts, sub_concepts, concept_comparison, template species inheritance, and life arc field requirements. Updated quick start example with v0.3 syntax including species and type system declarations.
262 lines
7.8 KiB
Rust
262 lines
7.8 KiB
Rust
//! 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 3 characters
|
|
let char_count = project.characters().count();
|
|
assert_eq!(char_count, 3, "Baker family should have 3 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"
|
|
);
|
|
}
|
|
|
|
#[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::Text(specialty)) = martha.fields.get("specialty") {
|
|
assert_eq!(
|
|
specialty, "sourdough",
|
|
"Martha's specialty should be sourdough"
|
|
);
|
|
} else {
|
|
panic!("specialty should be a Text");
|
|
}
|
|
}
|
|
|
|
#[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/templates.sb
|
|
// - behaviors/baker_behaviors.sb
|
|
// - schedules/work_schedules.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"
|
|
);
|
|
}
|