mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
The bridge and the host existed but nothing loaded them. Now the player does. restartPlayer() replaces every location.reload() call site. On BrightSign a page-initiated reload does not reliably bring the roHtmlWidget back, so the page asks the host to rebuild it and only falls back to reload() when no host is there to take the request. That covers the deploy path, the operator refresh, the service-worker activation and the manual reset. Identity now round-trips through the registry, which outlives localStorage on this platform: getConfig() adopts a registry identity when local storage comes back empty, instead of re-pairing and spawning a second row for a panel that is already provisioned. The operator reset clears the registry too — otherwise it would clear localStorage, get the same identity straight back on the next boot, and reset nothing. Registration reports platform 'brightsign' rather than "Chrome 120", which is what sync-backend.js resolves native-vs-ours from, plus model, OS, serial and which output this widget paints. Dual output needed a collision fix: autorun.brs gives the second HDMI output its own widget, and both widgets share an origin, a registry and one SD storage_path. Un-namespaced, output 2 would read output 1's config, install salt and device id and the two would collapse into a single device row. Storage keys and registry keys are now suffixed per output; screen 1 keeps the bare names so nothing existing moves. The bridge is served from its single source so the copy the player loads can never skew from the one on the SD card next to autorun.brs, and it is served to every player rather than gated on a user agent — a panel reporting an unexpected UA would otherwise silently lose restart-instead-of-reload. Two test harnesses extract player functions and run them in an isolated scope, so they now supply SCREEN_SUFFIX; one gained a case proving two outputs of one player get distinct identities. 927 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
175 lines
6.8 KiB
JavaScript
175 lines
6.8 KiB
JavaScript
'use strict';
|
|
|
|
// st-bridge.js is loaded by EVERY player, not just BrightSigns, because gating it on a user agent
|
|
// would mean a panel reporting an unexpected UA silently loses restart-instead-of-reload — the one
|
|
// thing it most needs. That makes its behaviour in a plain browser a correctness requirement, not a
|
|
// nicety: it must not throw, must report isBrightSign() false, and must tell the caller it could NOT
|
|
// take a restart so the player falls back to location.reload() instead of doing nothing.
|
|
//
|
|
// The other half is the dual-output collision. autorun.brs gives the second HDMI output its own
|
|
// widget, and both widgets share an origin, a registry and one SD storage_path. Un-namespaced keys
|
|
// would have output 2 read output 1's identity, and the two would collapse into a single device row
|
|
// — the same duplicate-row failure the hardware-only fingerprint once caused, in reverse.
|
|
//
|
|
// Run in a vm with a fake global rather than a browser, so the contract is checked without hardware.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const vm = require('node:vm');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const SRC = fs.readFileSync(path.join(__dirname, '..', '..', 'brightsign', 'st-bridge.js'), 'utf8');
|
|
|
|
/** Load the bridge into a fake window. `mods` present => pretend we are on a BrightSign. */
|
|
function load({ search = '', mods = null, ua = 'Mozilla/5.0 Chrome/150' } = {}) {
|
|
const posted = [];
|
|
const registryStore = new Map();
|
|
|
|
const sandbox = {
|
|
console: { log() {}, warn() {}, error() {} },
|
|
navigator: { userAgent: ua },
|
|
location: { search, reload() { sandbox.__reloaded = true; } },
|
|
setInterval: () => 1,
|
|
Date,
|
|
RegExp,
|
|
parseInt,
|
|
isNaN,
|
|
String,
|
|
decodeURIComponent,
|
|
__reloaded: false,
|
|
__posted: posted,
|
|
__registry: registryStore,
|
|
localStorage: { getItem: () => null, setItem() {} },
|
|
};
|
|
sandbox.window = sandbox;
|
|
|
|
if (mods) {
|
|
sandbox.require = (name) => {
|
|
if (name === '@brightsign/messageport') {
|
|
return function () {
|
|
return {
|
|
PostBSMessage: (o) => posted.push(o),
|
|
addEventListener: () => {},
|
|
};
|
|
};
|
|
}
|
|
if (name === '@brightsign/registry') {
|
|
return function () {
|
|
return {
|
|
read: (section, key) => registryStore.get(section + ':' + key),
|
|
write: (section, key, value) => registryStore.set(section + ':' + key, value),
|
|
};
|
|
};
|
|
}
|
|
if (name === '@brightsign/deviceinfo') {
|
|
return function () {
|
|
return { model: 'XT1145', osVersion: '9.1.92.2', serialNumber: 'SN-TEST-1' };
|
|
};
|
|
}
|
|
throw new Error('no such module ' + name);
|
|
};
|
|
}
|
|
|
|
vm.createContext(sandbox);
|
|
vm.runInContext(SRC, sandbox);
|
|
return { api: sandbox.ScreenTinkerBS, sandbox, posted, registryStore };
|
|
}
|
|
|
|
test('in a plain browser it loads without throwing and reports not-BrightSign', () => {
|
|
const { api } = load();
|
|
assert.equal(api.isBrightSign(), false);
|
|
assert.equal(api.hasHost(), false);
|
|
});
|
|
|
|
test('THE FALLBACK: with no host, restart() returns false so the player can reload instead', () => {
|
|
const { api } = load();
|
|
// Returning false is the whole contract — the player checks it and calls location.reload().
|
|
assert.equal(api.restart('deploy'), false);
|
|
});
|
|
|
|
test('off-platform accessors return null/defaults rather than throwing', () => {
|
|
const { api } = load();
|
|
assert.equal(api.serial(), null);
|
|
assert.equal(api.model(), null);
|
|
assert.equal(api.osVersion(), null);
|
|
assert.equal(api.screen(), 1);
|
|
assert.equal(api.storageSuffix(), '');
|
|
assert.equal(api.setVideoMode({ width: 1920 }), false);
|
|
assert.doesNotThrow(() => api.onHostMessage(null));
|
|
});
|
|
|
|
test('a BrightSign UA alone is enough to identify the platform', () => {
|
|
// A widget built without nodejs_enabled resolves no modules, but the player still needs to know.
|
|
const { api } = load({ ua: 'BrightSign/9.1.92.2 (HD1026) Chrome/120.0.6099.225' });
|
|
assert.equal(api.isBrightSign(), true);
|
|
assert.equal(api.hasHost(), false, 'no modules means no host to take a restart');
|
|
});
|
|
|
|
test('with the host present, restart() posts to BrightScript and reports success', () => {
|
|
const { api, posted } = load({ mods: true });
|
|
assert.equal(api.hasHost(), true);
|
|
assert.equal(api.restart('server code updated'), true);
|
|
const msg = posted.find((m) => m.type === 'restart');
|
|
assert.ok(msg, 'the host must actually receive it');
|
|
assert.equal(msg.reason, 'server code updated');
|
|
});
|
|
|
|
test('identity round-trips through the registry', () => {
|
|
const { api } = load({ mods: true });
|
|
api.setIdentity('dev-123', 'https://screentinker.com');
|
|
assert.equal(api.deviceId(), 'dev-123');
|
|
});
|
|
|
|
test('THE RESET: clearIdentity makes the registry forget, so a reset really resets', () => {
|
|
const { api, posted } = load({ mods: true });
|
|
api.setIdentity('dev-123', null);
|
|
api.clearIdentity();
|
|
assert.equal(api.deviceId(), null, 'otherwise the next boot re-adopts the same display');
|
|
assert.ok(posted.some((m) => m.type === 'identity' && m.clear === true));
|
|
});
|
|
|
|
test('THE COLLISION: output 2 namespaces its registry key and storage away from output 1', () => {
|
|
const one = load({ mods: true, search: '?screen=1' });
|
|
const two = load({ mods: true, search: '?screen=2' });
|
|
|
|
assert.equal(one.api.screen(), 1);
|
|
assert.equal(two.api.screen(), 2);
|
|
assert.equal(one.api.storageSuffix(), '', 'screen 1 must keep the bare keys — existing panels');
|
|
assert.equal(two.api.storageSuffix(), '_s2');
|
|
|
|
one.api.setIdentity('display-A', null);
|
|
two.api.setIdentity('display-B', null);
|
|
assert.equal(one.api.deviceId(), 'display-A');
|
|
assert.equal(two.api.deviceId(), 'display-B', 'two outputs must not collapse into one device row');
|
|
|
|
// and the underlying keys really are distinct
|
|
assert.deepEqual(
|
|
[...one.registryStore.keys()].sort(),
|
|
['screentinker:device_id']
|
|
);
|
|
assert.deepEqual(
|
|
[...two.registryStore.keys()].sort(),
|
|
['screentinker:device_id_s2']
|
|
);
|
|
});
|
|
|
|
test('deviceinfo supplies identity, with the URL as the fallback before modules resolve', () => {
|
|
const withMods = load({ mods: true });
|
|
assert.equal(withMods.api.serial(), 'SN-TEST-1');
|
|
assert.equal(withMods.api.model(), 'XT1145');
|
|
|
|
const urlOnly = load({ search: '?serial=SN-URL&model=XC2055', ua: 'BrightSign/9 Chrome/120' });
|
|
assert.equal(urlOnly.api.serial(), 'SN-URL');
|
|
assert.equal(urlOnly.api.model(), 'XC2055');
|
|
});
|
|
|
|
test('sync backend comes from the URL, else the registry, else auto', () => {
|
|
assert.equal(load({ mods: true }).api.syncBackend(), 'auto');
|
|
assert.equal(load({ mods: true, search: '?sync_backend=brightsign' }).api.syncBackend(), 'brightsign');
|
|
|
|
const persisted = load({ mods: true });
|
|
persisted.api.setSyncBackend('screentinker');
|
|
assert.equal(persisted.api.syncBackend(), 'screentinker', 'a cold boot with no network still starts right');
|
|
});
|