google/xb-testing
>- Write sequential asynchronous functional, integration, or simulator tests for xrblocks apps using the testing addon. Use this when you need to mock WebGL/WebAudio in headless environments (like JSDOM / Vitest), simulate user locomotion, trigger controller pointing, raycasts, and select/squeeze hand inputs. Covers `TestRunner` and `TestRunnerConfig`.
npx skills add https://github.com/google/xrblocks --skill xb-testing
The testing addon (import { TestRunner } from 'xrblocks/addons/testing') provides a headless functional test framework designed to test scripts, locomotion, interaction, and engine lifecycle sequentially under environments like Vitest/JSDOM.
Use TestRunner.create to spin up a core instance with a spied canvas/WebGL context:
import {describe, it, expect} from 'vitest';
import {TestRunner} from 'xrblocks/addons/testing';
import {MyScript} from './MyScript';
describe('My Functional Test', () => {
it('verifies script interaction', async () => {
const script = new MyScript();
// Create the runner and load scripts
const runner = await TestRunner.create({
scripts: [script],
});
// Step the frame loop forward (in milliseconds)
await runner.step(100);
// Check script states
expect(script.someValue).toBe(true);
// Always clean up to prevent memory/state leaks
await runner.destroy();
});
});
Simulate user camera translation (in strafe, rise, forward offsets relative to camera orientation):
// Move user forward by 1 meter
await runner.move([0, 0, -1], {durationMs: 200});
// Verify camera position changed
expect(runner.camera.position.z).toBeLessThan(0);
Simulate hands or controllers pointing at objects and performing selections (pinches/clicks):
// Point right hand (index 1) directly at the target object
await runner.pointTo(1, targetMesh);
// Perform a pinch/click with the right hand
await runner.click(1);
// Step time in ms to allow the select start and select end callbacks to fire
await runner.step(250);
expect(targetMesh.clicked).toBe(true);
Any error or exception thrown during script lifecycle (init, update, onSelectEnd, etc.) is caught by the test runner. Call step() or check for errors explicitly:
// Script crash verification
const crasher = new CrashingScript();
const runner = await TestRunner.create({scripts: [crasher]});
// Advancing the frame loop throws the exception caught in the script
await expect(runner.step(16.67)).rejects.toThrow(
'Script crash inside update loop'
);
TestRunner automatically stubs AudioContext, AudioListener parameter curves, and WebGLRenderer capabilities transparently.await runner.destroy() in your tests to reset the core singleton, otherwise subsequent tests will share state and leak listeners.Take google/xb-testing from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.