screentinker/server/test/player-capabilities.test.js
ScreenTinker 6bc709d2f7 The capability contract: what each player can actually do
Foundation for platform-native parity. The dashboard offered every control to
every display — a browser tab cannot reboot its host, a Tizen TV has no
device-owner concept, a BrightSign has no per-window brightness — so those
buttons did nothing, silently, and read as bugs. "UI that reports success and
changes nothing" is a recurring shape here; this ends it by letting the frontend
hide what a display cannot do.

The player DECLARES its capabilities at registration rather than the server
inferring them from a table, because only the player knows at runtime: an Android
device gains real screenshots when accessibility is switched on and loses Tier-2
commands when it is not device owner.

The trap this had to avoid is the opposite failure. Several hundred displays are
in the field declaring nothing, and none will update before the next dashboard
deploy — treating absence as "supports nothing" would strip the UI for the entire
fleet at once. So an ABSENT declaration falls back to a per-platform baseline,
while an EMPTY one is honoured as a player genuinely saying it can do nothing.
Those two cases are trivial to conflate and the difference is a dark dashboard.

Baselines carry only what has always worked on that platform. Anything
conditional — screenshots needing accessibility, kiosk needing device owner,
native sync needing one L2 network — is omitted, so a legacy display shows those
controls only once it declares them. A control that appears late beats one that
lies now.

Capability names are stable strings because they are persisted per device and
sent over the wire; renaming one silently disables a control on every display
still reporting the old name. An unknown name from a NEWER player is dropped
rather than invalidating the whole declaration.

1094 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-05 13:59:04 -05:00

92 lines
4.7 KiB
JavaScript

'use strict';
// The dashboard offered every control to every display. A browser tab cannot reboot its host, a
// Tizen TV has no device-owner concept, a BrightSign has no per-window brightness — so those
// buttons did nothing, silently. "UI that reports success and changes nothing" is a recurring bug
// shape here, and hiding an unsupported control is the fix.
//
// The risk in doing that is the opposite failure: stripping controls from the several hundred
// displays already in the field, none of which declare anything. So an ABSENT declaration falls
// back to a per-platform baseline, while an EMPTY one is honoured as a player genuinely saying it
// can do nothing. Those two cases are easy to conflate and the difference is a dark dashboard.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const caps = require('../lib/player-capabilities');
test('a legacy display with NO declaration keeps its platform baseline', () => {
// The several-hundred-device case: they will not update before the next dashboard deploy.
const android = { client_type: 'apk', android_version: '12' };
assert.ok(caps.supports(android, 'system.reboot'));
assert.ok(caps.supports(android, 'playback.video'));
});
test('THE DISTINCTION: an EMPTY declaration is honoured, not treated as missing', () => {
// A player saying "I can do nothing" is a real answer — a widget with no host bridge, say — and
// must not be quietly upgraded to the baseline.
const d = { client_type: 'apk', capabilities: '[]' };
assert.deepEqual(caps.capabilitiesFor(d), []);
assert.equal(caps.supports(d, 'playback.video'), false);
});
test('a declaration overrides the baseline in BOTH directions', () => {
// Android gains real screenshots only when accessibility is on, and loses Tier-2 commands when
// it is not device owner. A static table could never know either.
const restricted = { client_type: 'apk', capabilities: JSON.stringify(['playback.video']) };
assert.equal(caps.supports(restricted, 'system.reboot'), false, 'declared set is authoritative');
const enhanced = { platform: 'Chrome 120', capabilities: JSON.stringify(['playback.video', 'system.reboot']) };
assert.ok(caps.supports(enhanced, 'system.reboot'), 'a web player behind a host CAN reboot');
});
test('a browser tab does not claim what it cannot do', () => {
const web = { platform: 'Chrome 150', android_version: 'Web/Chrome' };
assert.equal(caps.supports(web, 'system.reboot'), false);
assert.equal(caps.supports(web, 'system.kiosk'), false);
assert.equal(caps.supports(web, 'display.power'), false);
assert.ok(caps.supports(web, 'playback.video'));
});
test('platform families are recognised from the same fields the rest of the product uses', () => {
assert.equal(caps.platformFamily({ platform: 'brightsign' }), 'brightsign');
assert.equal(caps.platformFamily({ platform: 'Tizen 6.0' }), 'tizen');
assert.equal(caps.platformFamily({ client_type: 'apk' }), 'android');
assert.equal(caps.platformFamily({ android_version: 'Android 12' }), 'android');
assert.equal(caps.platformFamily({ android_version: 'Web/Safari' }), 'web');
assert.equal(caps.platformFamily({}), 'web', 'unknown falls back to the most limited set');
});
test('an unknown capability from a NEWER player does not discard the ones we understand', () => {
const d = { client_type: 'apk', capabilities: JSON.stringify(['playback.video', 'quantum.teleport']) };
assert.deepEqual(caps.capabilitiesFor(d), ['playback.video']);
});
test('asking about a capability that does not exist is false, never a throw', () => {
assert.equal(caps.supports({ client_type: 'apk' }, 'nonsense.capability'), false);
assert.equal(caps.supports(null, 'playback.video'), false);
});
test('malformed declarations fall back rather than blanking the UI', () => {
for (const bad of ['not json', '{"a":1}', ' ', 42]) {
const d = { client_type: 'apk', capabilities: bad };
assert.ok(caps.supports(d, 'playback.video'), `${JSON.stringify(bad)} must fall back to baseline`);
}
});
test('every baseline entry is a real capability name', () => {
// A typo here silently disables a control for a whole platform.
for (const [family, list] of Object.entries(caps.BASELINE)) {
for (const c of list) assert.ok(caps.CAP_SET.has(c), `${family} baseline has unknown capability ${c}`);
}
});
test('BrightSign claims display power and reboot; Tizen claims neither', () => {
// The concrete parity facts this whole model exists to express.
const bs = { platform: 'brightsign' };
const tizen = { platform: 'Tizen 6.5' };
assert.ok(caps.supports(bs, 'display.power'));
assert.ok(caps.supports(bs, 'system.reboot'));
assert.equal(caps.supports(tizen, 'display.power'), false);
assert.equal(caps.supports(tizen, 'system.reboot'), false);
});