Expanded documentation image

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

ConceptWhat it is
ScenarioA scenario is essentially a test. Specifically, it is a class extending Scenario with an async run() method containing interactions and assertions.
Scenario indexA registry that maps a scenario ID to its file and optional parameters, allowing the framework to discover and run scenarios by ID.
InteractorA simulated input source that drives your Lens. See Available interactors below.
Assertionsexpect(...) checks inside run(). A failed assertion, or any thrown error, fails the scenario.

Available interactors

  1. DefaultLeafInteractor simulates generic input through a SIK interactor and can trigger, drag, and hover any SIK interactable. It is the simplest interactor.
  2. LeafHandInteractor simulates hand input with individually accessible hands for the left and right side. In addition, this interactor provides access to the Hand, which can perform various gestures as well through hand.makeGesture(gesture). Supported gestures are fist, pinch, palm, backhand, and relaxed.
  3. IkBodyInteractor simulates 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:

  1. Your Lens Studio project targets SPECS.
  2. Your Lens uses Spectacles Interaction Kit (SIK) interactables for the interactions you want to test.
  3. The Lens Studio project is open in the Lens Studio editor.

Install LEAF

LEAF ships as a single Leaf package in the Asset Library.

  1. In Lens Studio, open the Asset Library.
  2. Search for Leaf and import it into your project.
  3. Confirm that Leaf.lspkg appears 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-packages instead. 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.

  1. Create a new TypeScript file in your project, for example Assets/Tests/TapButtonScenario.ts.

  2. Create a class that extends Scenario and implement the async run() 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:

MethodWhat 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, and findInScene with custom matchers
  • Async helpers such as nextFrame() and sleep(ms) for waiting on scene updates between interactions

Register Your Scenarios

Scenarios become runnable by ID once they are registered in a scenario index.

  1. Create an index class with the @scenariosIndex decorator:
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(),
    },
  ];
}
  1. Attach the index script to a SceneObject in your scene so it initializes with the Lens.

  2. Optionally, add parameters to a scenario's metadata, for example parameters: {button_name: "PrimaryButton"}, and read them in run(config) with config.getOrThrow("button_name") or config.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.

  1. In Lens Studio, open the LEAF panel.
  2. Reset the Preview to ensure that the Lens starts from a clean state.
  3. Select a scenario from the list and run it.
  4. 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

  1. Open the Logger panel.
  2. A scenario that completes run() without throwing is reported as PASSED.
  3. 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.lspkg is in your Asset Browser.
  • Your scenario class extends Scenario, and every async call inside run() is awaited.
  • The scenario is registered in a @scenariosIndex index 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 await async calls inside run(). 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. trigger waits for the interactable's trigger events to fire, and drag runs for the full duration you pass. Use the waiting utilities nextFrame() or sleep(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, or hover API. You can still use LEAF for lenses using those, but you may need to implement your own custom Interactors.
  • While building, use /verify-preview for 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-scenarios analyzes your Lens and authors scenarios for its key interactions.
  • See Agents and Skills → Testing with LEAF for how to use LEAF with CLAD.