agentleFS
mirzaaghazadehmirzaaghazadeh/iwsdk-webxr-skills/skills/custom/iwsdk-quest-device-testing/SKILL.md

iwsdk-quest-device-testing

Skill1 starsChanged 30 days agoMITView on GitHub

Tests an IWSDK WebXR app on a physical Meta Quest in Quest Browser: developer mode and adb checks, the dev server over Wi-Fi or USB (`adb reverse`) and its certificate, the production build over HTTPS for benchmarks, getting results off the headset, remote DevTools via chrome://inspect, metavr logs and screenshots, the real frame rate, OVR Metrics, a headset-only checklist, and Meta VR Glasses prep on a Quest 3/3S (head-gaze preview, `metavr device fov-sim`, hands-only run). Use when the user says "test on my Quest", "open it in the headset", "Enter VR button missing", "works in the emulator but not on the Quest", "test the production build on the Quest", "save results from the headset", "kicked out of VR when I save", "inspect Quest Browser", "remote debug", "adb reverse", "headset logs", "what FPS is it running at", "test for Meta VR Glasses without the device", "simulate the glasses field of view", or before any store build. For scripted control of a real session, hand off to `iwsdk-native-xr-test`.

  • Reads credentials
  • Sends data out

The file

Show its rules and text

We split this skill into the rules it gives an agent

16 rules. About 0 are common, 7 are distinctive, and 9 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈7

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Ask before installing Android platform tools, OVR Metrics Tool, or anything on the headset.

    line 43
  2. Distinctive:

    Keep the dev server on the local network or loopback.

    line 44
  3. Very specific:

    Remove adb reverse routes when done.

    line 46
  4. Distinctive:

    Check from the computer:

    line 54
  5. Very specific:

    Start the app: npx @iwsdk/cli dev up (or npm run dev) and read the URLs:

    line 59
  6. Very specific:

    Use the port dev status reports (scaffold default 8081).

    line 75
  7. Distinctive:

    Use Console for errors and your debug logs, Network for 404s, CORS failures and asset sizes, Performance for CPU traces (DevTools adds …

    line 91
  8. Very specific:

    After any launch, check metavr adb shell "pidof <pkg>", or look for ForegroundAppHandler: Changing in focus immersive app ...

    line 124
  9. Very specific:

    Add a dev-only probe after World.create(...) resolves (remove it before release).

    line 129
  10. Very specific:

    Don't assume a default refresh rate: Meta's frame-rate page only gives older-device defaults.

    line 166
  11. Very specific:

    Read frameRate, and see iwsdk-performance before calling updateTargetFrameRate.

    line 167
  12. Distinctive:

    Use a port the dev server doesn't use, and accept the certificate warning once more (new port).

    line 195
  13. Very specific:

    Add results/ to .gitignore.

    line 243
  14. Distinctive:

    If the view doesn't change, take the headset off and on, or sleep/wake it, or relaunch the page.

    line 289
  15. Distinctive:

    Hands only: Put the controllers down (the session switches to hands) and complete every required flow: entering XR, menus, dialogs, …

    line 293
  16. Very specific:

    Don't follow world.features.fieldOfViewMask from Meta's hosted IWSDK guide: IWSDK 1.0.1 rejects the key …

    line 304

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Testing an IWSDK app on a Meta Quest

Made by Navid Mirzaaghazadeh · part of iwsdk-webxr-skills · MIT

The emulator (IWER) proves the code paths. Only the headset proves input feel, comfort, text legibility, real frame rate, thermals and mixed-reality features. Do this early and after every feature, not just before release.

Related: scripted control of a real session → iwsdk-native-xr-test (official); frame-time problems → iwsdk-performance; Quest CLI details → docs/metavr-cli-reference.md in this library and the metavr-cli skill if installed.

Ground rules

  • Work with one explicit device serial. If adb devices lists several, ask which one and pass -s <serial> to every adb command.
  • Ask before installing Android platform tools, OVR Metrics Tool, or anything on the headset.
  • Keep the dev server on the local network or loopback. Never expose it with a public tunnel without asking (it serves your source and, with AI tooling, a command bridge).
  • Remove adb reverse routes when done. Never automate permission dialogs in the headset; ask the person wearing it.

0. Headset ready?

  1. Developer mode is on: Meta Horizon mobile app → headset icon → your headset → Headset Settings → Developer Mode (Meta's device setup page). Connect a USB-C data cable, put the headset on, and choose Always allow from this computer at the USB debugging prompt.
  2. Check from the computer:
    adb devices                         # want: <serial>  device   (not unauthorized/offline)
    npm exec metavr -- device list      # same via the project's metavr dev dependency
  3. Start the app: npx @iwsdk/cli dev up (or npm run dev) and read the URLs:
    npx @iwsdk/cli dev status           # data.runtimeUrls.local and data.runtimeUrls.network[]

1. Open the app on the headset

Pick one route. IWER is not injected in Quest Browser (userAgentException defaults to /OculusBrowser/), so the headset always uses its native WebXR.

RouteStepsWhen
A. Same Wi-Fi (IWSDK's recommended method)Open a data.runtimeUrls.network URL, e.g. https://192.168.1.20:8081/, in Quest BrowserHome networks
B. USB + adb reverseadb -s <serial> reverse tcp:8081 tcp:8081, then open https://localhost:8081/Corporate/guest Wi-Fi, client isolation, firewalls
C. Chrome port forwardingchrome://inspect/#devices → Port forwarding… → add the portSame as B, from Chrome's UI
  • Use the port dev status reports (scaffold default 8081).
  • Open the URL without typing in the headset: npm exec metavr -- device browser "https://localhost:8081/".
  • The dev certificate is self-signed: accept the warning once per host. That's expected.
  • Choose Enter XR. The scaffold sets world.xr.offer: "always": IWSDK calls navigator.xr.offerSession(...) after start-up and again after each session ends, so Quest Browser shows its own enter button; an app may also add its own.
  • Clean up route B: adb -s <serial> reverse --remove tcp:8081.

2. Remote DevTools

USB (ADB) connection required.

  1. Open the page in Quest Browser.
  2. On the computer, open chrome://inspect/#devices in desktop Chrome.
  3. Find the headset and the Quest Browser tab → inspect.

Use Console for errors and your debug logs, Network for 404s, CORS failures and asset sizes, Performance for CPU traces (DevTools adds overhead; compare runs under the same conditions). Check the console first: type errors can stop a system from initialising without any visible symptom.

3. Logs and captures from the device

npm exec metavr -- log -n 200 -l W                          # recent device log, warnings and up
npm exec metavr -- capture screenshot -o artifacts/quest.png
adb -s <serial> logcat -d | tail -200                       # raw logcat, if metavr isn't installed

metavr log is a shortcut for adb logcat with tag and level filters. Whether Quest Browser mirrors page console.* output into logcat is unverified; rely on chrome://inspect for page logs. metavr options: npm exec metavr -- <command> --help.

Recording logcat for a whole session (metavr 1.8, observed 2026-10-25 on a Quest 3S):

  • Piping metavr adb logcat -f ... or metavr adb shell "logcat -T 1 ..." into a file on the computer wrote nothing, even after minutes.
  • What worked was letting the headset write the file: metavr adb shell "nohup logcat -v time -f /sdcard/Download/session.log -r 8192 -n 2 VrApi:I AndroidRuntime:E '*:S' > /dev/null 2>&1 &", then metavr adb pull /sdcard/Download/session.log when needed. The tested run also had a Unity:V tag; add the tags your app logs under. VrApi gives the FPS= lines.
  • metavr adb -- logcat ... is rejected ("unexpected argument"); write metavr adb logcat or metavr adb shell "logcat ...".

A launch that never happened (installed TWA or any APK; metavr 1.8, observed 2026-10-25):

  • metavr app launch <pkg> --cold-start --verify printed "Launched" and "no crash signal", yet pidof <pkg> was empty. Horizon OS had held the launch back. Logcat said Launch is blocked because: a Reprojected OS dialog is currently showing. Caching launch, then Interstitial session (APP_RESUME) took more than 15s and appLaunchFailureBroadcastReceived.
  • The app starts once the person in the headset closes the dialog.
  • After any launch, check metavr adb shell "pidof <pkg>", or look for ForegroundAppHandler: Changing in focus immersive app ... to (<pkg>).

4. Read the real session facts

Add a dev-only probe after World.create(...) resolves (remove it before release). frameRate, supportedFrameRates and updateTargetFrameRate are WebXR APIs that Quest Browser 16.4+ supports; they may be missing from TypeScript's types, hence the cast.

import { VisibilityState } from '@iwsdk/core';

type RateSession = XRSession & {
  frameRate?: number;
  supportedFrameRates?: Float32Array;
  updateTargetFrameRate?: (rate: number) => Promise<void>;
};

if (import.meta.env.DEV) {
  world.visibilityState.subscribe((state) => {
    if (state !== VisibilityState.Visible) return;
    const s = world.session as RateSession | undefined;
    console.log('[xr] mode/features', s?.enabledFeatures,
      'frameRate', s?.frameRate,
      'supported', s?.supportedFrameRates && Array.from(s.supportedFrameRates));
  });

  let sum = 0;
  let frames = 0;
  world.onXRFrame((_frame, delta) => {   // delta is seconds
    sum += delta;
    if (++frames === 300) {
      console.log('[xr] avg frame', ((sum / frames) * 1000).toFixed(2), 'ms',
        'draw calls', world.renderer.info.render.calls,
        'triangles', world.renderer.info.render.triangles);
      sum = 0;
      frames = 0;
    }
  });
}

Don't assume a default refresh rate: Meta's frame-rate page only gives older-device defaults. Read frameRate, and see iwsdk-performance before calling updateTargetFrameRate.

4a. Test the production build

Frame-rate and load-time numbers should come from the production build: the dev server serves unbundled modules, the HMR client and emulator code. vite preview can serve dist/ over HTTPS with the certificate iwsdkDev() already made (.iwsdk/https/_cert.pem holds both the key and the certificate):

// vite.config.ts
import { existsSync, readFileSync } from 'node:fs';

const devCert = '.iwsdk/https/_cert.pem';                 // written by iwsdkDev() on first dev run
const previewHttps = existsSync(devCert)
  ? { key: readFileSync(devCert), cert: readFileSync(devCert) }
  : undefined;

export default defineConfig({
  // ...existing config
  preview: { host: '0.0.0.0', port: 8083, https: previewHttps },
});
npm run build && npx vite preview        # then open https://<LAN-IP>:8083/ in Quest Browser

Use a port the dev server doesn't use, and accept the certificate warning once more (new port). Checked with IWSDK 1.0.1 on 2026-10-16 from the computer (the HTTPS page and the §4b endpoint both answer); opening it in Quest Browser is unverified.

4b. Get results off the headset without DevTools

A Quest page can't write files, and copying from the remote console is slow. A small Vite plugin gives the page a same-origin endpoint that saves JSON (benchmark summaries, per-second perf rows, tuning exports) into the repo, on both the dev server and vite preview:

// vite-plugins/results-sink.ts
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import type { Connect, Plugin } from 'vite';

export function resultsSink(root: string): Plugin {
  const dir = join(root, 'results');
  const handler: Connect.NextHandleFunction = (req, res) => {
    const kind = (req.url ?? '').replace(/^\/+/, '').split('?')[0];
    if (req.method !== 'POST' || !/^[a-z0-9-]{1,40}$/.test(kind)) {
      res.statusCode = 400;
      return res.end('expected POST /__results/<kind>');
    }
    const chunks: Buffer[] = [];
    let size = 0;
    req.on('data', (c: Buffer) => { size += c.length; if (size <= 5 * 1024 * 1024) chunks.push(c); });
    req.on('end', () => {
      try {
        if (size > 5 * 1024 * 1024) throw new Error('payload too large');
        const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
        mkdirSync(dir, { recursive: true });
        const file = join(dir, `${kind}-${new Date().toISOString().replace(/[:.]/g, '-')}.json`);
        writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`);
        res.end(JSON.stringify({ saved: file }));
      } catch (err) { res.statusCode = 400; res.end(String(err)); }
    });
  };
  return {
    name: 'results-sink',
    configureServer: (server) => { server.middlewares.use('/__results/', handler); },
    configurePreviewServer: (server) => { server.middlewares.use('/__results/', handler); },
  };
}
// vite.config.ts: plugins: [iwsdkDev(), resultsSink(process.cwd())]
// in the app:     fetch('/__results/bench', { method: 'POST', body: JSON.stringify(summary) })

Add results/ to .gitignore. The endpoint exists only on the dev and preview servers (a static host has none), and it accepts writes from anyone who can reach the port, so keep those servers on the LAN.

5. Performance HUD

OVR Metrics Tool shows FPS and CPU/GPU frame times as an in-headset overlay. Install it from Meta Quest Developer Hub or sideload it (Meta's WebXR performance workflow page), with the user's permission. Record: target vs. actual FPS, GPU frame time, CPU frame time, in the heaviest scene of the app, after 5+ minutes (thermals). Deeper tracing (Perfetto via npm exec metavr -- perf capture) is covered by Meta's hz-perfetto-debug skill if installed; whether it attributes Quest Browser WebXR frames usefully is unverified.

6. What only the headset proves

  • Session starts from the app's button and from Quest Browser's own prompt
  • Controllers: trigger/select, squeeze/grab, thumbsticks, A/B/X/Y, menu
  • Hand tracking (requires world.xr.features.handTracking in iwsdk.config.json)
  • Locomotion comfort: teleport, snap/smooth turn, vignette; no player falling through floors
  • Text and UI readable at the real viewing distance; panels not head-locked
  • Holds the target frame rate in the heaviest scene; no stutter on asset loads
  • Mixed reality: passthrough, room setup present, planes/meshes/anchors/hit-test/depth as requested
  • Audio: spatial direction and volume
  • Exit XR and re-enter: state resets correctly; ScreenSpace UI comes back
  • Headset sleep / wake (proximity sensor) and system menu: app pauses on VisibleBlurred

7. Preparing for Meta VR Glasses on a Quest

The glasses ship in spring 2027; Meta's advice until then is to test on a Quest 3 or 3S. Their users aim with their eyes, pinch to select, often have no controllers, and see 70° × 66°. App changes (config, gaze UI, controller-free flows) are in iwsdk-vr-glasses; this is the headset routine.

  1. Head-directed gaze preview. With world.xr.features.gazeTracking set, add "dev": { "targetDevicePreview": { "gazeSimulation": "head" } } to iwsdk.config.json (IWSDK 1.0.1; dev server only, needs dev.emulator.iwer left on). The native Quest Browser session keeps its viewer, hands, controllers and frame timing, plus a test gaze source that follows the head. The console's [iwsdk][gaze] lines show whether gaze was granted, missing, invalid or head-mapped. It proves plumbing, not eye-tracking accuracy.
  2. Field-of-view simulation (ask first: it changes a headset setting until disabled or rebooted). Needs a developer-mode Quest 3, 3S or Pro:
    npm exec metavr -- device fov-sim enable              # nominal 70° × 66°
    npm exec metavr -- device fov-sim enable --perceived  # p90 perceived 58° × 54°
    npm exec metavr -- device fov-sim disable             # always restore afterwards
    If the view doesn't change, take the headset off and on, or sleep/wake it, or relaunch the page. Check the boundary through the lenses: casts and screenshots only show the crop on Horizon OS v209+, and before v209 the simulation doesn't clip compositor layers (WebXR quad/cylinder layers).
  3. Hands only. Put the controllers down (the session switches to hands) and complete every required flow: entering XR, menus, dialogs, scrolling, grabbing, locomotion, pause and exit. Then pick the controllers up mid-flow and put them down again: focus, held objects and open dialogs must stay valid (Meta's VR Glasses test procedure).
  4. Layout. In the simulated view, check the first view after launch and after a recenter, menus, captions, error messages, and targets near each edge. Note anything clipped or reachable only by an unexpected head turn.
  5. Sustained run. Heaviest scene for the content length or 45 minutes (§5), so the result can be compared once VR Glasses hardware (with its separate glasses and puck thermals) is available.

Don't follow world.features.fieldOfViewMask from Meta's hosted IWSDK guide: IWSDK 1.0.1 rejects the key (docs/meta-reference/web-iwsdk-docs-on-meta.md).

Troubleshooting

SymptomCheck
No Enter XR button / isSessionSupported falsePage not on HTTPS or localhost; world.xr disabled (--target browser projects have xr: false); a required XR feature the device lacks
localhost refused on the headsetadb reverse missing, wrong serial, or wrong port; check adb -s <serial> reverse --list
Network URL times outDifferent Wi-Fi, client isolation, or firewall on 8081 → route B
Certificate page every timeHost changed (new IP); accept again, or use route B for a stable localhost
IWER toolbar shows on the headsetdev.emulator.activation set to always, a custom userAgentException, or injectOnBuild: true
Works in IWER, black screen in headsetRead the console via chrome://inspect; then a full page reload (HMR can leave a stale session)
Dropped out of VR whenever a file is savedExpected in dev: an edit reloads the page, which ends the XR session (headset and emulator). Batch edits, warn whoever wears the headset, re-enter XR; play-test on vite preview (§4a)
Assets missing only on deviceNetwork tab: 404 from a hard-coded /path (use import.meta.env.BASE_URL), or CORS on a CDN
device unauthorizedRe-plug and accept the USB debugging prompt in the headset
Dev server rejects iwsdk.config.json after following Meta's VR Glasses page (unknown property "fieldOfViewMask")That key isn't in IWSDK 1.0.1; remove it and use §7 (also suppressWhenHandPointerActive → suppressWhenDirectPointerActive)
Headset's field of view stays narrowmetavr device fov-sim disable, then take the headset off and on (a reboot also clears it)
metavr app launch says "Launched" but nothing opensAn OS dialog is showing in the headset (logcat: Launch is blocked because). Ask the wearer to close it; confirm with pidof <pkg> (§3)

Sources

Last verified: 2026-10-25 (§3 logcat and launch notes); 2026-10-17 (the rest)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.