Files
Pierre Kisters 4034fd4ded VR pet: a cat or dog in SteamVR's scene (steamFrame.pet)
A SteamVR dashboard patch (vr-pet) drawing a baked 3D pet next to the
windows: the Toon Cat in five coats, a Shiba Inu, a Fox and a Dachshund,
plus models from steamFrame.pet.extraModels. It walks around, follows,
sits, lies and sleeps, can be picked up by its grip bar, petted and
switched in its menu; its spot, pose and model are saved state.

Also the vr-pet command, "Pet" in the "+" menu with the current model's
icon (steam-frame-nix-pet-icon), the flake outputs pet-models,
pet-icons and pet-preview, and the check pet. A built-in model with
"private": true fails evaluation; private models belong in extraModels.
2026-10-01 04:33:39 +02:00

132 lines
7.9 KiB
JavaScript

// Tests of the vr-pet CLI (cli.mjs) with a fake DevTools transport.
// usage: node cli.test.mjs (exit 1 on failure)
// Run by the flake check `pet` (package.nix `tests`).
import { run, expression, API_VERSION, EXIT, Unreachable } from '../cli.mjs';
const TARGETS = [
{ title: 'Steam', webSocketDebuggerUrl: 'ws://x/steam' },
{ title: 'systemui', webSocketDebuggerUrl: 'ws://x/systemui' },
];
// A transport answering `value` (the page's reply object) for the systemui target.
const fake = ({ targets = TARGETS, value, reply, listError, evalError, hang } = {}) => {
const calls = [];
return {
calls,
async list(endpoint) { calls.push(['list', endpoint]); if (listError) throw listError; return targets; },
async evaluate(target, expr) {
calls.push(['evaluate', target.title, expr]);
if (hang) return new Promise(() => {});
if (evalError) throw evalError;
return reply ?? { id: 1, result: { result: { type: 'object', value } } };
},
};
};
// The page side of the expression, evaluated against a fake window.
const evalIn = async (expr, window) => new Function('window', `return ${expr};`)(window);
let failed = 0;
const tests = [];
const test = (name, f) => tests.push([name, f]);
const assert = (c, msg) => { if (!c) throw new Error(msg); };
test('usage errors exit 4', async () => {
for (const argv of [[], ['toggle'], ['show', '--now'], ['hide', '--summon'], ['status', 'x'], ['model'], ['model', 'a', 'b'], ['model', '"x'], ['models', 'x']]) {
const t = fake();
const r = await run(argv, { transport: t });
assert(r.code === EXIT.error && r.code === 4 && /usage/.test(r.err[0]), `${argv.join(' ')}: ${r.code}`);
assert(t.calls.length === 0, 'no DevTools call on a usage error');
}
});
test('show: evaluates only __sfuiPet.api.show on the systemui page', async () => {
const t = fake({ value: { status: 'ok', value: { hidden: false, was: 'hidden', summoned: false } } });
const r = await run(['show'], { transport: t });
assert(r.code === 0 && r.out[0] === 'vr-pet: shown', `${r.code} ${r.out}`);
const [, title, expr] = t.calls[1];
assert(title === 'systemui', `target ${title}`);
assert(/window\.__sfuiPet\?\.api/.test(expr) && /api\["show"\]\(\{"summon":false\}\)/.test(expr), expr);
assert(!/document|VRHTML|click|dispatch/.test(expr), 'nothing but the api');
const s = await run(['show', '--summon'], { transport: fake({ value: { status: 'ok', value: { was: 'shown', summoned: true } } }) });
assert(s.code === 0 && /already shown \(summoned/.test(s.out[0]), s.out[0]);
});
test('the expression: not injected, wrong version, ok (against a fake page)', async () => {
const e = expression('show', { summon: true });
assert((await evalIn(e, {})).status === 'not-injected', 'not injected');
assert((await evalIn(e, { __sfuiPet: { api: { version: API_VERSION + 1 } } })).status === 'version', 'version');
let got;
const w = { __sfuiPet: { api: { version: API_VERSION, show: async (a) => { got = a; return { was: 'hidden' }; } } } };
const v = await evalIn(e, w);
assert(v.status === 'ok' && v.value.was === 'hidden' && got.summon === true, JSON.stringify(v));
});
test('hide and status', async () => {
let r = await run(['hide'], { transport: fake({ value: { status: 'ok', value: { hidden: true, was: 'shown' } } }) });
assert(r.code === 0 && r.out[0] === 'vr-pet: hidden', r.out[0]);
r = await run(['hide'], { transport: fake({ value: { status: 'ok', value: { hidden: true, was: 'hidden' } } }) });
assert(r.out[0] === 'vr-pet: already hidden', r.out[0]);
r = await run(['status'], { transport: fake({ value: { status: 'ok', value: { hidden: false, pose: 'sit' } } }) });
assert(r.code === 0 && JSON.parse(r.out[0]).pose === 'sit', r.out[0]);
});
test('unreachable: no endpoint, no systemui page, timeout -> 2', async () => {
let r = await run(['status'], { transport: fake({ listError: new Unreachable('refused') }) });
assert(r.code === 2 && /refused/.test(r.err[0]), `refused: ${r.code}`);
r = await run(['status'], { transport: fake({ listError: new Error('bug') }) });
assert(r.code === EXIT.error, `a transport bug: ${r.code}`);
r = await run(['status'], { transport: fake({ targets: [TARGETS[0]] }) });
assert(r.code === 2 && /no "systemui" page/.test(r.err[0]), `${r.code} ${r.err}`);
r = await run(['status'], { transport: fake({ targets: {} }) });
assert(r.code === 2, `bad list: ${r.code}`);
const t0 = Date.now();
r = await run(['status'], { transport: fake({ hang: true }), timeoutMs: 50 });
assert(r.code === 2 && /no answer/.test(r.err[0]) && Date.now() - t0 < 1000, `timeout: ${r.code} ${r.err}`);
// the real transport against a closed port
r = await run(['status'], { endpoint: 'http://127.0.0.1:9', timeoutMs: 2000 });
assert(r.code === 2, `closed port: ${r.code} ${r.err}`);
});
test('not injected / wrong version -> 3; page exception -> 4', async () => {
let r = await run(['show'], { transport: fake({ value: { status: 'not-injected' } }) });
assert(r.code === 3, `not injected: ${r.code}`);
r = await run(['show'], { transport: fake({ value: { status: 'version', version: 9 } }) });
assert(r.code === 3 && /version 9/.test(r.err[0]), `version: ${r.code} ${r.err}`);
r = await run(['show'], { transport: fake({ reply: { id: 1, result: { exceptionDetails: { text: 'Uncaught', exception: { description: 'TypeError: boom' } } } } }) });
assert(r.code === 4 && /boom/.test(r.err[0]), `exception: ${r.code} ${r.err}`);
r = await run(['show'], { transport: fake({ reply: { id: 1, error: { message: 'Target closed' } } }) });
assert(r.code === 4, `protocol error: ${r.code}`);
r = await run(['show'], { transport: fake({ value: 'unchanged' }) });
assert(r.code === 4, `odd reply: ${r.code}`);
});
test('models: lists the coats, the current one starred, names aligned past the longest id; only api.models is called', async () => {
const t = fake({ value: { status: 'ok', value: { current: 'tuxedo', default: 'ginger', models: [{ id: 'ginger', name: 'Ginger' }, { id: 'tuxedo', name: 'Tuxedo' }, { id: 'dachshund', name: 'Dachshund' }] } } });
const r = await run(['models'], { transport: t });
const want = [' ginger Ginger', '* tuxedo Tuxedo', ' dachshund Dachshund'];
assert(r.code === 0 && JSON.stringify(r.out) === JSON.stringify(want), JSON.stringify(r.out));
assert(/api\["models"\]\(\{\}\)/.test(t.calls[1][2]), t.calls[1][2]);
});
test('model <id>: api.setModel({ id }); unknown -> 4 with the list; API version 2', async () => {
assert(API_VERSION === 2, `API_VERSION ${API_VERSION}`);
const t = fake({ value: { status: 'ok', value: { model: 'blue', was: 'ginger', name: 'Blue' } } });
let r = await run(['model', 'blue'], { transport: t });
assert(r.code === 0 && r.out[0] === 'vr-pet: now Blue', `${r.code} ${r.out}`);
assert(/api\["setModel"\]\(\{"id":"blue"\}\)/.test(t.calls[1][2]), t.calls[1][2]);
r = await run(['model', 'blue'], { transport: fake({ value: { status: 'ok', value: { model: 'blue', was: 'blue', name: 'Blue' } } }) });
assert(r.code === 0 && r.out[0] === 'vr-pet: already Blue', r.out[0]);
r = await run(['model', 'blue'], { transport: fake({ value: { status: 'ok', value: { model: 'blue', was: 'ginger', name: 'Blue', hidden: true } } }) });
assert(/hidden/.test(r.out[0]), r.out[0]);
r = await run(['model', 'lion'], { transport: fake({ value: { status: 'ok', value: { error: 'unknown model "lion"', model: 'blue', models: ['ginger', 'blue'] } } }) });
assert(r.code === 4 && /unknown model "lion" \(models: ginger, blue\)/.test(r.err[0]), `${r.code} ${r.err}`);
// an old page (API 1): not ours to call
r = await run(['models'], { transport: fake({ value: { status: 'version', version: 1 } }) });
assert(r.code === 3, `old API: ${r.code}`);
});
for (const [name, f] of tests) {
try { await f(); console.log(`ok ${name}`); } catch (e) { failed++; console.log(`FAIL ${name}: ${e.stack}`); }
}
if (failed) { console.log(`${failed} failed`); process.exit(1); }
console.log('all passed');