install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/gpui-testing" ~/.claude/skills/majiayu000-claude-skill-registry-gpui-testing && rm -rf "$T"
manifest:
skills/data/gpui-testing/SKILL.mdsource content
gpui-testing
Comprehensive guide to testing GPUI applications based on patterns from script-kit-gpui.
Overview
GPUI applications require specific testing patterns because they deal with:
- UI state management - Views, components, state machines
- Async operations - Background tasks, channels, spawn
- Platform integration - Hotkeys, system calls, OS-specific behavior
- Serialization - Config files, theme parsing, protocol messages
Test File Organization
Convention: *_tests.rs
Files
*_tests.rsTests are organized in separate
*_tests.rs files alongside their implementation:
src/ theme/ mod.rs # Theme types and logic theme_tests.rs # Theme tests config/ mod.rs # Config types config_tests.rs # Config tests components/ unified_list_item.rs unified_list_item_tests.rs
Import tests in the module:
// In mod.rs #[cfg(test)] mod theme_tests;
Inline Test Modules
For smaller modules, use inline test modules:
#[cfg(test)] mod tests { use super::*; #[test] fn test_something() { // ... } }
Test Setup Patterns
Helper Functions
Create helpers to reduce test boilerplate:
/// Helper to create a test Scriptlet with minimal required fields fn test_scriptlet(name: &str, tool: &str, code: &str) -> Scriptlet { Scriptlet { name: name.to_string(), description: None, code: code.to_string(), tool: tool.to_string(), shortcut: None, expand: None, group: None, file_path: None, command: None, alias: None, } } /// Helper to wrap Vec<Script> into Vec<Arc<Script>> for tests fn wrap_scripts(scripts: Vec<Script>) -> Vec<Arc<Script>> { scripts.into_iter().map(Arc::new).collect() } /// Helper to create a shortcut with modifiers fn make_shortcut(key: &str, cmd: bool, shift: bool) -> Shortcut { Shortcut { key: key.to_string(), modifiers: Modifiers { cmd, shift, ..Default::default() }, } }
Test Fixtures
For complex types, create fixture factories:
fn create_test_config() -> Config { Config { hotkey: HotkeyConfig { modifiers: vec!["meta".to_string()], key: "Semicolon".to_string(), }, bun_path: None, editor: None, padding: None, // ... other fields with defaults } }
Testing Pure Logic (No GPUI Context)
Default Values
#[test] fn test_default_config() { let config = Config::default(); assert_eq!(config.hotkey.modifiers, vec!["meta"]); assert_eq!(config.hotkey.key, "Semicolon"); assert_eq!(config.bun_path, None); }
Serialization Roundtrip
#[test] fn test_config_serialization() { let config = Config { /* ... */ }; let json = serde_json::to_string(&config).unwrap(); let deserialized: Config = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.hotkey.key, config.hotkey.key); }
JSON Deserialization Edge Cases
#[test] fn test_config_deserialization_minimal() { let json = r#"{ "hotkey": { "modifiers": ["meta"], "key": "Semicolon" } }"#; let config: Config = serde_json::from_str(json).unwrap(); assert_eq!(config.bun_path, None); // Missing fields default } #[test] fn test_hex_color_parse_multiple_formats() { assert_eq!(parse_color_string("#FBBF24").unwrap(), 0xFBBF24); assert_eq!(parse_color_string("0xFBBF24").unwrap(), 0xFBBF24); assert_eq!(parse_color_string("rgb(251, 191, 36)").unwrap(), 0xFBBF24); }
Testing State Machines
Registry Pattern
#[test] fn register_and_get() { let mut registry = ShortcutRegistry::new(); let binding = ShortcutBinding::builtin( "test.action", "Test Action", make_shortcut("k", true, false), ShortcutContext::Global, ShortcutCategory::Actions, ); registry.register(binding); let retrieved = registry.get("test.action").unwrap(); assert_eq!(retrieved.name, "Test Action"); } #[test] fn user_override_takes_precedence() { let mut registry = ShortcutRegistry::new(); registry.register(/* ... */); let shortcut = registry.get_shortcut("test.action").unwrap(); assert_eq!(shortcut.key, "k"); // Default registry.set_override("test.action", Some(make_shortcut("j", true, true))); let shortcut = registry.get_shortcut("test.action").unwrap(); assert_eq!(shortcut.key, "j"); // Overridden }
Channel Testing
#[test] fn hotkey_channels_are_independent() { // Clear channels while hotkey_channel().1.try_recv().is_ok() {} while script_hotkey_channel().1.try_recv().is_ok() {} // Send to one channel hotkey_channel().0.send_blocking(()).expect("send hotkey"); // Verify other channel is empty assert!(matches!( script_hotkey_channel().1.try_recv(), Err(TryRecvError::Empty) )); // Verify original channel has message assert!(hotkey_channel().1.try_recv().is_ok()); }
Testing Components
Type Verification
#[test] fn test_list_item_colors_is_copy() { // Compile-time verification that type implements Copy fn assert_copy<T: Copy>() {} assert_copy::<ListItemColors>(); }
Layout Calculations
#[test] fn test_density_comfortable_layout() { let layout = ListItemLayout::from_density(Density::Comfortable); assert_eq!(layout.height, 48.0); assert!(layout.padding_x >= 12.0); } #[test] fn test_layout_height_is_fixed() { let comfortable = ListItemLayout::from_density(Density::Comfortable); let compact = ListItemLayout::from_density(Density::Compact); assert!(comfortable.height > 0.0); assert!(compact.height > 0.0); assert!(comfortable.height > compact.height); }
UTF-8 Safety
#[test] fn test_split_by_ranges_emoji_safe() { // "a[emoji]b" - emoji is 4 bytes let text = "a\u{1F600}b"; let ranges = vec![1..5]; // The emoji bytes let spans = split_by_ranges(text, &ranges); assert_eq!(spans[0], ("a", false)); assert_eq!(spans[1], ("\u{1F600}", true)); assert_eq!(spans[2], ("b", false)); } #[test] fn test_split_by_ranges_japanese() { // Each Japanese char is 3 bytes let text = "\u{65E5}\u{672C}\u{8A9E}"; // "nihongo" let ranges = vec![3..6]; // Middle character let spans = split_by_ranges(text, &ranges); assert_eq!(spans[1].0.chars().next().unwrap(), '\u{672C}'); }
System Tests with Feature Flags
Use
#[cfg(feature = "system-tests")] for tests that:
- Require OS permissions (accessibility, hotkeys)
- Open real windows/applications
- Interact with system services
#[cfg(feature = "system-tests")] #[test] fn test_handle_get_selected_text_returns_handled() { let msg = Message::get_selected_text("req-001".to_string()); let result = handle_selected_text_message(&msg); // ... } // This test actually opens Finder #[cfg(all(unix, feature = "system-tests"))] #[test] fn test_run_scriptlet_open() { let scriptlet = Scriptlet::new( "Open Test".to_string(), "open".to_string(), "/tmp".to_string(), ); let result = run_scriptlet(&scriptlet, ScriptletExecOptions::default()); assert!(result.is_ok()); }
Run system tests with:
cargo test --features system-tests
Platform-Specific Tests
#[cfg(unix)] #[test] fn test_spawn_and_kill_process() { let result = spawn_script("sleep", &["10"], "[test:sleep]"); if let Ok(mut session) = result { assert!(session.is_running()); session.kill().expect("kill should succeed"); std::thread::sleep(std::time::Duration::from_millis(100)); assert!(!session.is_running()); } } #[cfg(target_os = "macos")] #[test] fn test_find_conflicts_detects_os_reserved() { let mut registry = ShortcutRegistry::new(); // Cmd+Tab is OS reserved on macOS registry.register(ShortcutBinding::builtin( "app.switcher", "App Switcher", make_shortcut("tab", true, false), ShortcutContext::Global, ShortcutCategory::System, )); let conflicts = registry.find_conflicts(); assert!(conflicts.iter().any(|c| c.conflict_type == ConflictType::Unreachable )); }
Environment Variable Testing
Handle env var tests carefully (they're global):
#[test] fn test_get_editor_from_env() { // Save current value let original_editor = std::env::var("EDITOR").ok(); // Test with custom value std::env::set_var("EDITOR", "emacs"); let config = Config::default(); assert_eq!(config.get_editor(), "emacs"); // Restore original value match original_editor { Some(val) => std::env::set_var("EDITOR", val), None => std::env::remove_var("EDITOR"), } }
For parallel-safe env testing, test all cases sequentially in one test:
#[test] fn test_is_auto_submit_enabled_all_cases() { std::env::set_var("AUTO_SUBMIT", "true"); assert!(is_auto_submit_enabled()); std::env::set_var("AUTO_SUBMIT", "1"); assert!(is_auto_submit_enabled()); std::env::set_var("AUTO_SUBMIT", "false"); assert!(!is_auto_submit_enabled()); std::env::remove_var("AUTO_SUBMIT"); assert!(!is_auto_submit_enabled()); }
Code Audit Tests
Test invariants about the codebase itself:
#[test] fn test_no_direct_cx_hide_in_app_execute() { let content = fs::read_to_string("src/app_execute.rs") .unwrap_or_default(); let matches = find_lines_with_pattern(&content, "cx.hide()"); assert!( matches.is_empty(), "Found forbidden cx.hide() in app_execute.rs. Use self.close_and_reset_window(cx) instead." ); } #[test] fn test_close_and_reset_window_exists() { let content = fs::read_to_string("src/app_impl.rs") .unwrap_or_default(); let count = content.matches("fn close_and_reset_window").count(); assert!(count >= 1, "close_and_reset_window() not found"); }
Testing GPUI Keystroke Matching
#[test] fn find_match_respects_context_order() { let mut registry = ShortcutRegistry::new(); registry.register(ShortcutBinding::builtin( "editor.enter", "Editor Enter", make_shortcut("enter", false, false), ShortcutContext::Editor, ShortcutCategory::Actions, )); registry.register(ShortcutBinding::builtin( "global.enter", "Global Enter", make_shortcut("enter", false, false), ShortcutContext::Global, ShortcutCategory::Actions, )); let keystroke = gpui::Keystroke { key: "enter".to_string(), key_char: None, modifiers: gpui::Modifiers::default(), }; // Editor context first - should match editor binding let contexts = [ShortcutContext::Editor, ShortcutContext::Global]; assert_eq!( registry.find_match(&keystroke, &contexts), Some("editor.enter") ); }
Testing with GlobalHotKeyManager
System hotkey tests may fail in CI (no event loop):
mod script_hotkey_manager_tests { fn create_test_manager() -> Option<ScriptHotkeyManager> { // May fail in test environment GlobalHotKeyManager::new() .ok() .map(ScriptHotkeyManager::new) } #[test] fn test_manager_creation() { if let Some(manager) = create_test_manager() { assert!(manager.hotkey_map.is_empty()); } // If creation failed, test passes (expected in CI) } #[test] fn test_register_tracks_mapping() { if let Some(mut manager) = create_test_manager() { let result = manager.register("/test/script.ts", "cmd+shift+t"); if result.is_ok() { assert!(manager.is_registered("/test/script.ts")); } // Registration may fail without event loop - that's OK } } }
Anti-patterns
DON'T: Use cx.run()
in Unit Tests
cx.run()GPUI context methods require a running app. Unit tests should test pure logic.
DON'T: Rely on Global State
// BAD - tests may interfere with each other static mut COUNTER: i32 = 0; #[test] fn test_increment() { unsafe { COUNTER += 1; } // ... }
DON'T: Hardcode Paths
// BAD let path = "/Users/john/.scriptkit/scripts/test.ts"; // GOOD - use temp dirs let temp_dir = tempfile::tempdir().unwrap(); let path = temp_dir.path().join("test.ts");
DON'T: Forget Platform Guards
// BAD - will fail on Windows #[test] fn test_unix_signals() { use libc::kill; // ... } // GOOD #[cfg(unix)] #[test] fn test_unix_signals() { use libc::kill; // ... }
DON'T: Skip Cleanup
// BAD #[test] fn test_with_env() { std::env::set_var("MY_VAR", "value"); // Test crashes - env var leaked } // GOOD #[test] fn test_with_env() { let original = std::env::var("MY_VAR").ok(); std::env::set_var("MY_VAR", "value"); // ... test code ... // Always cleanup match original { Some(v) => std::env::set_var("MY_VAR", v), None => std::env::remove_var("MY_VAR"), } }
Running Tests
# Run all tests cargo test # Run tests for a specific module cargo test theme_tests # Run tests with system features cargo test --features system-tests # Run tests with output cargo test -- --nocapture # Run a single test cargo test test_default_config