Test Your Lens With LEAF
Install LEAF, write your first automated test scenario, and run it against your Lens in the Lens Studio preview.
Use this guide to install LEAF (Lens Evaluation & Automation Framework), write your first automated test scenario, and run it against your Lens in the Lens Studio preview.
LEAF is an automated testing framework for SPECS Lenses. It lets you define repeatable test scenarios, simulate user interactions such as hand input, gestures, and ray-based targeting, and catch bugs in your Lens's workflows. The result is less manual testing and more reliable releases.
The easiest way to use LEAF is through CLAD, which automates the process of installing, authoring, and running scenarios for your Lens:
/specs-leaf-install-packages: installs the LEAF framework./specs-leaf-write-scenarios: authors test scenarios for your Lens./specs-leaf-run-in-preview: runs the scenarios.
Understand the Building Blocks
| Concept | What it is |
|---|---|
| Scenario | A scenario is essentially a test. Specifically, it is a class extending Scenario with an async run() method containing interactions and assertions. |
| Scenario index | A registry that maps a scenario ID to its file and optional parameters, allowing the framework to discover and run scenarios by ID. |
| Interactor | A simulated input source that drives your Lens. See Available interactors below. |
| Assertions | expect(...) checks inside run(). A failed assertion, or any thrown error, fails the scenario. |
Available interactors
DefaultLeafInteractorsimulates generic input through a SIK interactor and can trigger, drag, and hover any SIK interactable. It is the simplest interactor.LeafHandInteractorsimulates hand input with individually accessible hands for the left and right side. In addition, this interactor provides access to theHand, which can perform various gestures as well throughhand.makeGesture(gesture). Supported gestures are fist, pinch, palm, backhand, and relaxed.IkBodyInteractorsimulates full-body inverse kinematics and can be used for the most realistic interactions.
If the available interactors don't cover all your interaction needs, you can always create your own custom interactor as well.
Before You Start
Before you begin, make sure:
- Your Lens Studio project targets SPECS.
- Your Lens uses Spectacles Interaction Kit (SIK) interactables for the interactions you want to test.
- The Lens Studio project is open in the Lens Studio editor.
Install LEAF
LEAF ships as a single Leaf package in the Asset Library.
- In Lens Studio, open the Asset Library.
- Search for Leaf and import it into your project.
- Confirm that
Leaf.lspkgappears in your Asset Browser.
Note: LEAF depends on SpectaclesInteractionKit. Match the LEAF, SIK, and Lens Studio versions to avoid compile-time or runtime mismatches.
Tip: with CLAD, run
/specs-leaf-install-packagesinstead. It installs LEAF and matches it to your SIK version automatically.
Write a Scenario
A scenario uses an interactor to drive the scene, then asserts on the result.
-
Create a new TypeScript file in your project, for example
Assets/Tests/TapButtonScenario.ts. -
Create a class that extends
Scenarioand implement the asyncrun()method:
import { Scenario } from "Leaf.lspkg/Scenarios/scenario/Scenario";
import { DefaultLeafInteractor } from "Leaf.lspkg/Interactors/interactor/DefaultLeafInteractor";
import { findInteractableByName } from "Leaf.lspkg/Interactors/InteractableUtils";
import { findSceneObjectByName } from "Leaf.lspkg/Utils/common/Utils";
import { expect } from "Leaf.lspkg/Utils/common/Expect";
@component
export class TapButtonScenario extends Scenario {
// Prefix every async call with `await` so failures throw
// instead of being silently swallowed.
async run(): Promise<void> {
const interactor = new DefaultLeafInteractor();
// Find the interactable to test.
const button = findInteractableByName("PrimaryButton");
expect(button).toBeTruthy();
// Simulate the interaction.
await interactor.trigger(button);
// Assert on the result.
const label = findSceneObjectByName("StatusLabel").getComponent("Text").text;
expect(label).toBe("Pressed");
}
}
Core interactions
The interactor supports three core interactions:
| Method | What it simulates |
|---|---|
trigger(target) | Targets and triggers an interactable. |
drag(target, dragVector, dragDurationMs) | Triggers an interactable, then drags it along a specified vector for a given duration (in milliseconds). |
hover(target, hoverDurationMs?) | Targets and hovers over an interactable for a given duration (in milliseconds, optional and defaults to 200ms). |
Assertions and utilities
After interacting with your Lens, your scenario needs to assert that the Lens reacts as expected. For that, you can add expect blocks after each interaction. You can find examples in the scenario shown above:
// Assert on the result.
const label = findSceneObjectByName("StatusLabel").getComponent("Text").text;
expect(label).toBe("Pressed");
expect() supports toBe, toEqual for deep equality, toBeCloseTo, toBeGreaterThan, toBeLessThan, toBeTruthy, toBeFalsy, toBeNull, and .not to invert any of them.
The utilities also include:
- Scene queries such as
findSceneObjectByName,findInteractableByName, andfindInScenewith custom matchers - Async helpers such as
nextFrame()andsleep(ms)for waiting on scene updates between interactions
Register Your Scenarios
Scenarios become runnable by ID once they are registered in a scenario index.
- Create an index class with the
@scenariosIndexdecorator:
import { scenariosIndex } from "Leaf.lspkg/Scenarios/decorator/ScenarioIndexDecorator";
import { ScenarioMetadata } from "Leaf.lspkg/Scenarios/scenario/ScenarioMetadata";
import { TapButtonScenario } from "./TapButtonScenario";
@component
export class LeafIndex extends BaseScriptComponent {
@scenariosIndex
static scenariosIndex: ScenarioMetadata[] = [
{
id: "tap_button",
typename: TapButtonScenario.getTypeName(),
},
];
}
-
Attach the index script to a SceneObject in your scene so it initializes with the Lens.
-
Optionally, add parameters to a scenario's metadata, for example
parameters: {button_name: "PrimaryButton"}, and read them inrun(config)withconfig.getOrThrow("button_name")orconfig.getNumber(...)to reuse one scenario class across multiple tests.
Run Scenarios in the Preview
Lens Studio includes a pre-installed plugin that you can use to run LEAF scenarios in the Preview. You can configure parameters for your scenario runs.
- In Lens Studio, open the LEAF panel.
- Reset the Preview to ensure that the Lens starts from a clean state.
- Select a scenario from the list and run it.
- Watch the preview. You will see the simulated interactions play out against your Lens.
Tip: with CLAD, run
/specs-leaf-run-in-preview. It opens the LEAF panel, lists your scenarios, and runs them for you. This is the fastest iteration loop while developing.
Read the Results
- Open the Logger panel.
- A scenario that completes
run()without throwing is reported as PASSED. - A scenario that throws, for example because of a failed
expect, a missing interactable, or a timeout, is reported as FAILED, together with the error in the log.
Quick Checklist
You are done when:
Leaf.lspkgis in your Asset Browser.- Your scenario class extends
Scenario, and every async call insiderun()is awaited. - The scenario is registered in a
@scenariosIndexindex attached to the scene. - The scenario appears in the LEAF panel and runs in the preview.
- The Logger shows PASSED, and shows FAILED when you intentionally break an assertion.
Important Considerations
- Always
awaitasync calls insiderun(). Un-awaited failures are silently swallowed, and the scenario may pass incorrectly. - If an interactable is not found, the interactor throws and the scenario fails. Prefer stable SceneObject names for the objects you test.
- Interactions are event-driven, not instant.
triggerwaits for the interactable's trigger events to fire, anddragruns for the full duration you pass. Use the waiting utilitiesnextFrame()orsleep(ms)when your Lens needs time to react before you assert. - LEAF simulates SIK interactors. Interactions that bypass SIK interactables, such as raw touch events or custom gesture code, are not directly testable with the
trigger,drag, orhoverAPI. You can still use LEAF for lenses using those, but you may need to implement your own custom Interactors. - While building, use
/verify-previewfor quick "did my last edit work?" checks. It writes nothing. Once the experience is far enough along that you want regression protection, write LEAF scenarios. A useful rule of thumb is: building means/verify-preview; locking in means LEAF.
Next Steps
- Let CLAD write your test suite:
/specs-leaf-write-scenariosanalyzes your Lens and authors scenarios for its key interactions. - See Agents and Skills → Testing with LEAF for how to use LEAF with CLAD.