Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add Nghi-NV/create-agent-skills --skill "lumi-tester"
Install specific skill from multi-skill repository
# Description
Comprehensive guide for extending the Lumi Tester framework. Use when adding new commands, selectors, or debugging the test runner.
# SKILL.md
name: lumi-tester
description: Comprehensive guide for extending the Lumi Tester framework. Use when adding new commands, selectors, or debugging the test runner.
Lumi Tester Development
This skill provides a step-by-step guide for extending the Lumi Tester automation framework. It covers adding new commands, selectors, and debugging workflows.
When to Use This Skill
- Use when adding a new automation command (e.g.,
scanBarcode,zoomIn). - Use when adding a new selector strategy (e.g.,
barcode: "...",ai_description: "..."). - Use when debugging command execution or driver issues.
- Use when understanding the architecture of
lumi-tester.
Architecture Overview
For a deep dive into the internal logic, see Core Architecture & Internals.
- Parser (
src/parser/types.rs): deserializes YAML toTestCommandenums. - Executor (
src/runner/executor.rs): orchestrates execution, managing context (TestContext) and flow (TestSessionState). Handles logic around the driver (retries, logging, flow control). - Driver Trait (
src/driver/traits.rs): defines the abstractPlatformDriverinterface. Handles direct device interactions. - Implementations (
src/driver/<platform>/driver.rs): concrete implementations for Android (ADB), iOS (IDB/WDA), and Web (Playwright).
How to use
Workflow 1: Add a New Command
Follow this strict order to ensure all layers are covered.
Step 1: Define Parser (src/parser/types.rs)
- Add a
structfor your command parameters (if it takes arguments).
rust #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] // Ensures camelCase in YAML maps to struct fields pub struct MyNewCommandParams { pub target: String, #[serde(default)] pub duration: Option<u64>, /// Optional label for custom logging (e.g., "Press Login Button") #[serde(default)] pub label: Option<String>, } - Add a variant to the
TestCommandenum.
rust #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum TestCommand { // ... #[serde(alias = "myCmd")] // Optional alias for YAML convenience MyNewCommand(MyNewCommandParams), // or MyNewCommand, if no params }
Step 2: Define Interface (src/driver/traits.rs)
- Add the method signature to the
PlatformDrivertrait.
rust #[async_trait] pub trait PlatformDriver: Send + Sync { // ... async fn my_new_command(&self, target: &str, duration: Option<u64>) -> Result<()>; } - (Optional) Provide a default implementation returning
Err("Not implemented")if it's not supported on all platforms immediately.
Step 3: Integrate Executor (src/runner/executor.rs)
- Locate
async fn execute_command. - Add a match arm for your new command variant.
rust TestCommand::MyNewCommand(params) => { println!(" {} Executing MyNewCommand...", "โค".blue()); // Verify selector if needed // let selector = self.build_selector(..., params.ocr, ...)?; self.driver.my_new_command(¶ms.target, params.duration).await?; }
Step 4: Implement Driver Logic (src/driver/<platform>/driver.rs)
Implement the trait method for EACH supported platform (android, ios, web).
Android (src/driver/android/driver.rs):
async fn my_new_command(&self, target: &str, duration: Option<u64>) -> Result<()> {
// Use adb or uiautomator
adb::exec_command(&self.serial, &["shell", "input", ...]).await?;
Ok(())
}
iOS (src/driver/ios/driver.rs):
async fn my_new_command(&self, target: &str, duration: Option<u64>) -> Result<()> {
// Use idb or WDA
self.wda_client.lock().await.perform_action(...).await?;
Ok(())
}
Web (src/driver/web/driver.rs):
async fn my_new_command(&self, target: &str, duration: Option<u64>) -> Result<()> {
let page = self.page.lock().await;
page.evaluate_js(...).await?;
Ok(())
}
Step 5: Update Documentation (docs/commands.md)
Add your new command to lumi-tester/docs/commands.md.
- Add to the Table of Contents if necessary.
- Add a new section with Description, Example YAML, and Parameters Table.
Step 6: Update VS Code Extension (lumi-tester-vscode)
Update src/schema/commands.ts in the lumi-tester-vscode repo to enable IntelliSense.
-
Add a new object to the
LUMI_COMMANDSarray:
typescript { name: 'myNewCommand', category: 'Interaction', // or 'App Management', 'Control Flow'... description: 'Description of what it does', hasParams: true, snippet: 'myNewCommand:\n target: "$1"', // VS Code snippet params: [ { name: 'target', type: 'string', description: 'Target element' }, { name: 'duration', type: 'number', description: 'Duration in ms' }, // For complex objects, use a rich snippet in the param definition: { name: 'ocr', type: 'object', description: 'Find by OCR', snippet: 'ocr:\n text: "${1:text_to_find}"\n region: "${2|all,center|}"' } ] },[!TIP]
Rich Snippets: For complex object parameters (likeocr,permissions), add asnippetfield to the parameter definition. This allows users to tab through properties (e.g.,text,region) instead of just getting a generic completion.
Selectors allow finding elements in different ways (Text, ID, OCR, etc.).
Step 1: Define Selector (src/driver/traits.rs)
Add a variant to the Selector enum.
#[derive(Debug, Clone, PartialEq)]
pub enum Selector {
// ...
MySelector(String, usize), // args: query, index
}
Step 2: Update Parameter Structs (src/parser/types.rs)
If your selector needs specific parameters in command definitions (like ocr: true or ocr: { text: ... }), update the relevant parameter structs (e.g., TapParams, AssertParams).
pub struct TapParams {
// ...
pub my_selector: Option<String>,
}
Step 3: Builder Logic (src/runner/executor.rs)
Update the build_selector function signature and logic.
- Update function signature to accept your new parameter.
- Add logic to build the
Selector.
fn build_selector(
// ... existing params
my_param: &Option<String>, // Add your new param here
) -> Result<Selector> {
// ...
if let Some(query) = my_param {
return Ok(Selector::MySelector(query.clone(), 0));
}
// ...
}
[!IMPORTANT]
You MUST update ALL call sites ofbuild_selectorinexecutor.rs(e.g., insideTapOn,AssertVisible, etc.) to pass the new parameter. Usually pass&Noneor¶ms.my_param.
Step 4: Implement Finding Logic (src/driver/<platform>/driver.rs)
Update find_element (or equivalent internal finder) for each platform.
Android:
- Handle Selector::MySelector in find_node_by_selector (XML parsing) or find_element (UIA/OCR).
iOS:
- Handle Selector::MySelector in find_element_internal. Map to idb predicates or WDA class chains if possible.
Web:
- Update selector_to_playwright in src/driver/web/driver.rs to convert your selector to a Playwright-compatible string (CSS/XPath) OR handle it manually in find_element.
Workflow 3: Modifying Existing Commands
If you add a field to an existing command struct (e.g., adding label to TapParams):
Step 1: Update Struct (src/parser/types.rs)
Add the field with #[serde(default)] if optional.
pub struct TapParams {
// ...
#[serde(default)]
pub label: Option<String>,
}
[!WARNING]
CRITICAL: Check for Manual Parsing (src/parser/yaml.rs)
Some commands (likeLaunchApp,TapOn) are manually constructed insrc/parser/yaml.rsinsideparse_command_with_params.
If you add a field to the struct, you MUST update the initialization logic inyaml.rs.Also Check:
impl Defaultblocks insrc/parser/types.rs. If you add a field, update the default implementation.
Step 2: Update Usage
Update executor.rs or driver implementations to use the new field.
Step 3: Update Docs & VS Code
- Docs: Update the parameter table in
docs/commands.md. - VS Code: Add the new parameter to the
paramsarray inlumi-tester-vscode/src/schema/commands.ts.
[!IMPORTANT]
Standardize Selectors: If you add a new selector (e.g.,ocr,role,desc) totap, you MUST also add it to ALL other interaction and assertion commands that use selectors:
- Interactions:doubleTap,longPress,rightClick,type,tapAt
- Scroll:scrollTo,scrollUntilVisible
- Assertions:see,notSee,waitUntilVisible,waitNotSeeFailure to do this results in inconsistent autocomplete and frustrates users who expect the new selector to work everywhere.
Logging & Debugging
Logging Guidelines
- User-Facing Logs: Use
println!with colored output for high-level steps (e.g.,โค Executing...).- Info:
println!("{}", "โค Message".blue()) - Success:
println!("{}", "โ Message".green()) - Error:
println!("{}", "โ Message".red())
- Info:
- Debug Logs: Use the
logcrate macros (debug!,trace!). These are hidden by default and only show whenRUST_LOG=debugis set.- DO NOT use
println!for internal details. - Example:
rust // In your driver or executor log::debug!("Finding element with selector: {:?}", selector);
- DO NOT use
Examples
- Adding a Zoom Command: End-to-end walkthrough of adding a new command across all layers.
Troubleshooting
- Common Issues: See Common Pitfalls & Troubleshooting for detailed solutions to parsing and execution errors.
- Logs: Use
println!with colored output ("msg".blue()) for visibility.- Blue: Info/Action (
"โค Executing...") - Cyan: Detail (
" Tap at...") - Yellow: Warning
- Red: Error
- Blue: Info/Action (
- Run with Trace:
RUST_LOG=trace cargo run ... - Output Dir: Check the output directory (default
test_output/<timestamp>/) for screenshots and logs. - Unit Tests: Create a small unit test in the driver file to verify specific logic (e.g., regex parsing) without running a full simulator.
Checklist for Success
- [ ] Parser: Did you add
structandTestCommandvariant with correctserdeattributes? - [ ] Executor: Did you add the
matcharm? Did you updatebuild_selectorcalls if needed? - [ ] Trait: Did you add the method to
PlatformDriver? - [ ] Impl: Did you implement it for Android, iOS, and Web? (Or use
unimplemented!()). - [ ] Legacy Init: If modifying an existing command, did you check
src/parser/yaml.rsfor manual initialization? - [ ] Docs: Did you update
docs/commands.md? - [ ] VS Code: Did you update
lumi-tester-vscode/src/schema/commands.ts? - [ ] Build: Run
cargo checkto catch missing implementations. - [ ] Test: Verify with a minimal YAML file.
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.