mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Both players carried calls that compile, read correctly, and are documented to
do something else. Verified line by line against docs.brightsign.biz and
Samsung's Smart TV Filesystem reference; every fix below cites the doc that
proves it, and the linter has been extended so each one fails here next time.
TIZEN
The offline media cache could never have worked on a panel. Its adapter used
the deprecated Filesystem API in three ways the IDL rules out:
`tizen.filesystem.resolve()` is declared `void`, so `var dir = resolve(...)`
was always undefined and MediaCache.create() returned null on every panel in
the fleet; `openStream()` is asynchronous, so appendPart read `written` before
any callback could run and returned 0 forever; and `moveTo()` is asynchronous,
belongs on the parent directory, and takes (origin, destination) — it was
called on a file handle with the arguments transposed. Rewritten against the
5.0 synchronous FileSystemManager, which is genuinely synchronous and is what
the decision layer needs. A Tizen 4.0 panel now reports available() false
instead of being handed a cache that silently writes nothing.
Writes are now POSITIONED rather than appended at EOF. Power cut between a
write and the index save — the exact event this feature exists for — replayed
the last chunk, and an append landed it twice: a silently corrupt video that
promoted as complete. A positioned write makes the replay idempotent.
Three decision-layer bugs alongside it: a 206 with no readable Content-Range
fell back to Content-Length, which is the CHUNK length, so the first megabyte
of a 50MB video promoted as a complete 1MB asset; a 200 whose body was short of
its own Content-Length returned 'done'; and a server with no ETag or
Last-Modified was re-fetched from zero on every sweep, forever, on precisely
the marginal link this feature exists to be gentle on.
The volume slider was dead. The dashboard sends `{level: 0..1}`; this handler
read `value`/`volume` as a 0..100 percentage, so it matched nothing and logged
"no usable value in payload" on every slider move while the panel declared
audio.volume as working. Both halves had to move together — taking `level` as a
percentage turns 50% into 0.5%, which is inaudible and looks like a fix.
Verified by driving the real handler in headless Chrome, before and after.
BRIGHTSIGN
FindMemberFunction is documented as available only when
roDeviceInfo.HasFeature("FindMemberFunction") is true. It was called
unguarded from the capability probe and from host telemetry — both on the event
loop — so a player without the feature would have died within a minute of boot
and taken the display with it. The guard needed guarding.
The boot report never arrived. The host flushed its buffer straight after
Show(), before the page had been fetched, while the player correctly waits for
its socket before subscribing. Between two correct decisions every boot line
fell on the floor. The host now waits for the page's `probe`, and the bridge
buffers until a consumer registers.
offline.cache was claimed on `navigator.serviceWorker` being present. It is
present on a BrightSign widget and will not run a worker — our XT245 passes the
check and never fetches sw.js. Now requires a controller, matching the web
player. Removed from the brightsign baseline for the same reason.
display.resolution was claimed on @brightsign/videooutput, which has no
setMode at all; mode setting lives on @brightsign/videomodeconfiguration.
roStorageHotplug.GetStorages() answers "USB1:/" while GetStorageStatus() is
documented as unreliable for "USBn:" — feeding one to the other re-created the
bug the static fallback list exists to avoid, and only on the OS versions that
have the enumerator.
dual/clone output mode put two full-screen widgets on output ONE, on top of
each other, while output two stayed dark: roHtmlWidget has no output selector,
and a second output is addressed by its display_x/display_y within the
SetScreenModes canvas. Now positioned properly, or refused with a reason.
Also: a manifest missing sha256/size passed `invalid` into typed parameters, a
runtime error at the call the comment already described and did not prevent;
storage_quota was a string where the docs say use a double; and the comment
crediting brightsign_js_objects_enabled with gating require("@brightsign/*")
named the wrong flag — it is nodejs_enabled.
TESTS
The two suites that mattered most were the ones that passed while the code was
broken, because they asserted on source text or against a fake more correct
than the platform. The host-diagnostics regexes now execute the bridge; the
media-cache suite now drives the shipped adapter against a fake tizen.filesystem
written from Samsung's IDL. Ten new rules in the BrightScript linter, each
verified to fail against the source it was written to reject.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
384 lines
20 KiB
JavaScript
384 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
// BrightScript cannot be run, linted or type-checked here — the only interpreter is a player. So a
|
|
// call to an object that does not exist looks exactly like a call to one that does, right up until
|
|
// a display in the field stops working.
|
|
//
|
|
// That is not hypothetical. `brightsign/*.brs` shipped with a whole family of ROKU APIs in it —
|
|
// roFileSystem, roMessageDigest, PostFromStringWithRetry — because BrightScript is Roku's language
|
|
// and the two references read almost identically. Each one silently disabled a feature: the
|
|
// self-update path could never mark a package applied, verification returned false unconditionally
|
|
// and burned an attempt counter, and a snapshot request raised "Member function not found" from
|
|
// inside the event loop, taking the player down. None of it was visible from here.
|
|
//
|
|
// This is the cheapest thing that would have caught all of it: a deny-list of APIs that exist on
|
|
// Roku and not on BrightSign, plus the argument-shape mistakes that made calls compile and then do
|
|
// nothing. It cannot prove the scripts are right. It does stop these specific, expensive mistakes
|
|
// coming back — and every entry below was paid for once already.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const DIR = path.join(__dirname, '..', '..', 'brightsign');
|
|
const FILES = fs.readdirSync(DIR).filter((f) => f.endsWith('.brs'));
|
|
|
|
/** Source with comment lines stripped, so prose about a bug is not mistaken for the bug. */
|
|
function code(file) {
|
|
return fs.readFileSync(path.join(DIR, file), 'utf8')
|
|
.split('\n')
|
|
.filter((l) => !/^\s*'/.test(l))
|
|
.join('\n');
|
|
}
|
|
|
|
test('there are BrightScript files to check', () => {
|
|
assert.ok(FILES.length >= 2, `expected the host scripts, found ${FILES.join(', ')}`);
|
|
});
|
|
|
|
// Objects that exist on Roku and NOT on BrightSign. Verified against BrightSign's Object Reference,
|
|
// which lists every ro* object the platform has.
|
|
const ROKU_ONLY = [
|
|
['roFileSystem', 'use the global MoveFile / DeleteFile / CopyFile, or roByteArray to read'],
|
|
['roMessageDigest', 'use roHashGenerator("sha256"); Hash() answers with an roByteArray'],
|
|
['roUnzip', 'use roBrightPackage — roUnzip is not the reader for a player package'],
|
|
['roRegistryKey', 'use roRegistrySection'],
|
|
['roAssociativeArrayEx', 'plain roAssociativeArray'],
|
|
// roDeviceInfo.GetDeviceUniqueId() is real; a global of that name is not.
|
|
['roDataGramSocket', 'not a BrightSign object'],
|
|
];
|
|
|
|
for (const [obj, advice] of ROKU_ONLY) {
|
|
test(`no ${obj} — it does not exist on BrightSign (${advice})`, () => {
|
|
for (const f of FILES) {
|
|
assert.ok(!code(f).includes(obj), `${f} calls ${obj}, which is a Roku object. ${advice}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Methods that do not exist on the object they are called on.
|
|
const BAD_METHODS = [
|
|
['PostFromStringWithRetry', 'roUrlTransfer has no retry variant; AsyncPostFromString + roUrlEvent is how a POST body is read'],
|
|
['.Final(', 'roMessageDigest-era API; roHashGenerator returns the digest from Hash()'],
|
|
['OpenInputFile', 'no roFileSystem here; read with roByteArray.ReadFile'],
|
|
// Each of these reads as obviously-correct and is documented NOT to exist on BrightSign.
|
|
['SetMessagePort', 'the setter is SetPort(roMessagePort) — SetMessagePort is Roku'],
|
|
['.WaitEvent(', 'roMessagePort has WaitMessage(timeout_ms); WaitEvent is Roku'],
|
|
['.SetAlgorithm(', 'roHashGenerator takes its algorithm as a CONSTRUCTOR argument'],
|
|
['.VerifyPackage(', 'roBrightPackage has no VerifyPackage — hash the bytes yourself'],
|
|
['.UnpackAll(', 'roBrightPackage has Unpack(path) and UnpackFile(name, path)'],
|
|
['SetOrientation(vm', 'roVideoMode has no SetOrientation — rotation is SetScreenModes()[i].transform'],
|
|
];
|
|
|
|
for (const [needle, advice] of BAD_METHODS) {
|
|
test(`no ${needle.replace(/[.(]/g, '')} — ${advice}`, () => {
|
|
for (const f of FILES) {
|
|
assert.ok(!code(f).includes(needle), `${f} calls ${needle}. ${advice}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
test('MatchFiles is called with a DIRECTORY and a bare pattern', () => {
|
|
// The bug that made a deployment report an empty card while `dir SD:` listed the file. MatchFiles
|
|
// takes a directory plus a pattern and is documented to return nothing when the pattern contains
|
|
// a separator — so passing a path as the second argument answers "no" for every file that exists.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/MatchFiles\(([^)]*)\)/g)) {
|
|
const args = m[1].split(',').map((a) => a.trim());
|
|
assert.equal(args.length, 2, `${f}: MatchFiles needs exactly two arguments, got ${m[0]}`);
|
|
assert.ok(!/["'].*\/.*["']/.test(args[1]) && !/\+/.test(args[1]),
|
|
`${f}: the MatchFiles PATTERN must not contain a path separator — ${m[0]}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('Unpack() is never used as if it returned a boolean', () => {
|
|
// Unpack(path) is declared As Void. `if not package.Unpack(...)` is a type error, not an error
|
|
// check — and reads exactly like one. Success is proven by looking for an expected file instead.
|
|
for (const f of FILES) {
|
|
const src = code(f);
|
|
assert.ok(!/if\s+not\s+\w*\.?Unpack\s*\(/i.test(src),
|
|
`${f}: Unpack() returns Void — test for an extracted file rather than its return value`);
|
|
assert.ok(!/=\s*\w*\.?Unpack\s*\(/i.test(src), `${f}: Unpack() returns nothing to assign`);
|
|
}
|
|
});
|
|
|
|
test('Unpack() never targets a volume root', () => {
|
|
// "Providing a destination path of SD:/ will wipe all preexisting files from the card." Unpacking
|
|
// an update straight to the root would erase the player's provisioning and its whole content pool
|
|
// as a side effect of a routine upgrade.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/\.Unpack\(([^)]*)\)/g)) {
|
|
const arg = m[1].trim();
|
|
assert.ok(!/^(root|root\$)\s*\+\s*"\/"$/.test(arg) && !/^"[A-Z0-9]+:\/"$/.test(arg),
|
|
`${f}: ${m[0]} unpacks to a volume root, which DELETES everything already there. Stage it.`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('roVideoMode.SetMode() is called with exactly one argument', () => {
|
|
// SetMode(mode As String). A second argument is a "wrong number of function parameters" abort.
|
|
// Rotation belongs to SetScreenModes(), whose config carries the transform.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/\.SetMode\(([^)]*)\)/g)) {
|
|
assert.equal(m[1].split(',').length, 1, `${f}: ${m[0]} — SetMode takes one argument`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('GetStorageStatus is not called with a USBn: drive string', () => {
|
|
// Documented: "The results of the GetStorageStatus() method are unreliable when called with a
|
|
// USBn: parameter." The drive strings it understands are "USB:", "SD:", "SSD:", "SD2:/", "Flash:".
|
|
for (const f of FILES) {
|
|
assert.ok(!/GetStorageStatus\(\s*"USB\d/i.test(code(f)),
|
|
`${f}: GetStorageStatus is unreliable with USBn: — use "USB:"`);
|
|
}
|
|
});
|
|
|
|
test('a load-error is reported with its uri, not a url', () => {
|
|
// `url` is a key of download-request; a load-error carries `uri`. Reading the wrong one made the
|
|
// only diagnostic that names the failing resource print "invalid" every time.
|
|
for (const f of FILES) {
|
|
const src = code(f);
|
|
if (!src.includes('load-error')) continue;
|
|
assert.ok(!/\bdata\.url\b/.test(src), `${f}: a load-error names its resource in data.uri`);
|
|
}
|
|
});
|
|
|
|
test('parameters do not carry BOTH a type suffix and an As clause', () => {
|
|
// `filePath$ As String` is a shape the reference never sanctions, and a parse error would stop
|
|
// the script loading at all — the worst possible failure, since nothing would run to report it.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/(?:Function|Sub)\s+\w+\s*\(([^)]*)\)/g)) {
|
|
for (const param of m[1].split(',')) {
|
|
assert.ok(!/[$%!#&]\s+As\s+/i.test(param),
|
|
`${f}: parameter "${param.trim()}" has a type suffix and an As clause`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('the storage root is resolved by probing, not assumed', () => {
|
|
// Knowing only FLASH and SD meant that fitting real storage to a flash-booting player and moving
|
|
// the deployment onto it resolved every derived path to a volume that was not there.
|
|
const src = code('autorun.brs');
|
|
const fn = src.slice(src.indexOf('Function StorageRoot'));
|
|
for (const vol of ['SSD:', 'USB1:', 'FLASH:']) {
|
|
assert.ok(fn.slice(0, 900).includes(vol), `StorageRoot() must consider ${vol}`);
|
|
}
|
|
});
|
|
|
|
test('the widget storage path is absolute, on a real volume', () => {
|
|
// "/cache" carries no drive specifier, so the widget's local storage has nowhere to persist.
|
|
const src = code('autorun.brs');
|
|
assert.ok(!/storage_path:\s*"\/[^"]*"/.test(src),
|
|
'storage_path must name a volume — a bare "/path" is outside the writable volumes');
|
|
assert.match(src, /storage_path:\s*StorageRoot\(\)/);
|
|
});
|
|
|
|
test('no doubled quotes inside a string literal — BrightScript has no escape sequences', () => {
|
|
// The one that cost a player its boot. `"{""width"":"` is not an escaped quote; it is three
|
|
// adjacent string literals with no operator between them, and the compiler rejects the WHOLE
|
|
// FILE: "ScriptLoadError: Syntax Error. (compile error &h02) in SSD:/autorun.brs(196)". The
|
|
// display came up with nothing at all — not a broken feature, no player. A quote in a literal has
|
|
// to come from Chr(34).
|
|
for (const f of FILES) {
|
|
const lines = fs.readFileSync(path.join(DIR, f), 'utf8').split('\n');
|
|
lines.forEach((line, i) => {
|
|
if (/^\s*'/.test(line)) return;
|
|
// `""` hugged by non-delimiters is a literal trying to contain a quote; `, ""` or `("")` is
|
|
// simply an empty string argument and is fine.
|
|
assert.ok(!/[^\s(,=]""[^\s),]/.test(line),
|
|
`${f}:${i + 1}: a string literal cannot contain a quote — build it with Chr(34)\n ${line.trim()}`);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('every string literal on a line is closed', () => {
|
|
// An odd number of quotes is the same class of failure: it takes the whole script down, and
|
|
// nothing here can run it to find out.
|
|
for (const f of FILES) {
|
|
const lines = fs.readFileSync(path.join(DIR, f), 'utf8').split('\n');
|
|
lines.forEach((line, i) => {
|
|
if (/^\s*'/.test(line)) return;
|
|
const beforeComment = line.split(/\s'(?=(?:[^"]*"[^"]*")*[^"]*$)/)[0];
|
|
const quotes = (beforeComment.match(/"/g) || []).length;
|
|
assert.equal(quotes % 2, 0, `${f}:${i + 1}: unbalanced quotes\n ${line.trim()}`);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('file existence is tested with roReadFile, not MatchFiles', () => {
|
|
// MatchFiles is for LISTING a directory. Used as an existence check it has burned us twice: once
|
|
// passing a full path as both arguments (never true for anything), and once passing a directory
|
|
// plus a bare name, which STILL answered "no" for a file sitting in a volume root on a real
|
|
// XT245 — silently skipping a staged update on every boot. BrightSign's own boilerplate uses
|
|
// roReadFile + type(); so do we.
|
|
for (const f of FILES) {
|
|
const src = code(f);
|
|
const helper = src.slice(src.indexOf('Function FileExists'));
|
|
if (!helper) continue;
|
|
assert.match(helper.slice(0, 300), /roReadFile/,
|
|
`${f}: FileExists must use roReadFile — MatchFiles does not answer reliably for a volume root`);
|
|
}
|
|
});
|
|
|
|
test('Str() is never applied to something that is already a string', () => {
|
|
// Str(value As Float). Handing it a string is a type error that aborts wherever it runs — and the
|
|
// place this was reached from is the event loop, i.e. it would take the player down while
|
|
// reporting a diagnostic. Message keys documented as String need no conversion at all.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/\bStr\((\w+(?:\.\w+)*)\)/g)) {
|
|
assert.ok(/%$|^\d/.test(m[1]) || /count|len|size|retries|attempts/i.test(m[1]),
|
|
`${f}: Str(${m[1]}) — Str is for numbers; a String needs no conversion`);
|
|
}
|
|
}
|
|
});
|
|
|
|
/* ---------------------------------------------------------------------------------------------
|
|
* Rules added after a line-by-line pass against docs.brightsign.biz. Each one is a call that
|
|
* compiles, reads correctly, and is documented to do something other than what it looks like.
|
|
* ------------------------------------------------------------------------------------------- */
|
|
|
|
test('roVideoMode.SetOrientation does not exist — rotation is a SetScreenModes transform', () => {
|
|
// Zero occurrences of SetOrientation in the roVideoMode reference. Rotation has two documented
|
|
// routes and neither is a method on roVideoMode: the per-screen `transform` member of
|
|
// SetScreenModes ("normal"/"90"/"180"/"270"), or roHtmlWidget's `transform` init parameter
|
|
// ("identity"/"rot90"/"rot180"/"rot270") — note the two use DIFFERENT vocabularies.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/(\w+)\.SetOrientation\s*\(/g)) {
|
|
assert.fail(`${f}: ${m[0]} — roVideoMode has no SetOrientation. Use SetScreenModes()[i].transform`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('FindMemberFunction is itself feature-gated before it is used as a guard', () => {
|
|
// "It is only available if roDeviceInfo.HasFeature("FindMemberFunction") returns true." Calling it
|
|
// on a player without the feature is a runtime error — and both call sites here are reached from
|
|
// the EVENT LOOP (the boot capability probe, and host telemetry every 60s), so on such a player
|
|
// the host would die within a minute and take the display with it. The guard needed guarding.
|
|
for (const f of FILES) {
|
|
const src = code(f);
|
|
if (!src.includes('FindMemberFunction(')) continue;
|
|
assert.match(src, /HasFeature\("FindMemberFunction"\)/,
|
|
`${f}: FindMemberFunction is only available when roDeviceInfo.HasFeature("FindMemberFunction") is true`);
|
|
// ...and every call must sit behind that check, not merely somewhere in the same file.
|
|
for (const m of src.matchAll(/^(?!.*HasFeature).*\bFindMemberFunction\(/gm)) {
|
|
const line = m[0];
|
|
assert.ok(/HasFindMember\(\)|HasFeature/.test(src.slice(Math.max(0, src.indexOf(line) - 400), src.indexOf(line))),
|
|
`${f}: this FindMemberFunction call is not behind a HasFeature guard:\n ${line.trim()}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('GetStorageStatus is never handed a drive string straight from GetStorages()', () => {
|
|
// The two speak DIFFERENT vocabularies. GetStorages() answers ["USB1:/", "SD:/", "SD2:/", "SSD:/",
|
|
// "Flash:/"] — trailing slash, USB numbered — while GetStorageStatus() understands "USB:", "SD:",
|
|
// "SSD:", "SD2:/", "Flash:" and is documented as UNRELIABLE for "USBn:". Feeding the enumerator's
|
|
// output back in re-creates the bug the static fallback list exists to avoid, and only on the OS
|
|
// versions that HAVE the enumerator — so it looks fine in testing.
|
|
for (const f of FILES) {
|
|
const src = code(f);
|
|
if (!src.includes('GetStorageStatus')) continue;
|
|
for (const m of src.matchAll(/GetStorageStatus\(([^)]*)\)/g)) {
|
|
const arg = m[1].trim();
|
|
assert.ok(/^"/.test(arg) || /StatusDrive\(/.test(arg),
|
|
`${f}: ${m[0]} — normalise the drive first (StatusDrive) or pass a literal; GetStorages() speaks a different vocabulary`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('PostJSMessage keys are lowercase — BrightScript canonicalises them on the way to JS', () => {
|
|
// BrightScript associative-array keys created with object-literal syntax are case-INSENSITIVE and
|
|
// arrive lowercased on the JavaScript side. BrightSign's own sample sends
|
|
// `{serialNumber: ...}` and reads `msg["serialnumber"]`. A camelCase key here is therefore a field
|
|
// the bridge reads as undefined, silently, with no error anywhere.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/PostJSMessage\(\{([\s\S]*?)\}\)/g)) {
|
|
for (const km of m[1].matchAll(/(?:^|[,\n])\s*([A-Za-z_]\w*)\s*:/g)) {
|
|
assert.equal(km[1], km[1].toLowerCase(),
|
|
`${f}: PostJSMessage key "${km[1]}" is not lowercase — it arrives in JavaScript lowercased and the reader sees undefined`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('PostJSMessage payloads are flat — nested associative arrays are not supported', () => {
|
|
// "This method does not support passing nested associative arrays." A nested value is dropped, so
|
|
// the message arrives looking well-formed and missing the part that mattered.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/PostJSMessage\(\{([\s\S]*?)\}\)/g)) {
|
|
assert.ok(!/:\s*\{/.test(m[1]),
|
|
`${f}: PostJSMessage carries a nested associative array, which BrightSign drops:\n ${m[0].slice(0, 160)}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('roUrlTransfer.SetPort takes a message port, never a TCP port number', () => {
|
|
// ifMessagePort.SetPort(port As roMessagePort). There is no integer overload — a TCP port belongs
|
|
// in the URL string. Passing a number here reads exactly like configuring a port and configures
|
|
// nothing.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/\.SetPort\(([^)]*)\)/g)) {
|
|
assert.ok(!/^\d+$/.test(m[1].trim()),
|
|
`${f}: ${m[0]} — SetPort takes an roMessagePort; put a TCP port in the URL`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('roBrightPackage is constructed with a filename string, not an associative array', () => {
|
|
// "created with a filename parameter that specifies the name of the .zip file". A password goes
|
|
// through SetPassword() afterwards. An AA here is the Roku-shaped guess.
|
|
for (const f of FILES) {
|
|
for (const m of code(f).matchAll(/CreateObject\("roBrightPackage"\s*,\s*([^)]*)\)/g)) {
|
|
assert.ok(!m[1].trim().startsWith('{'),
|
|
`${f}: ${m[0]} — roBrightPackage takes a filename String; use SetPassword() for a password`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('the widget storage quota is not a string', () => {
|
|
// "A BrightScript integer is only guaranteed to be able to represent a count of bytes up to 2GB so
|
|
// avoid using integers... Use float or double instead... (string can also be used but is not
|
|
// recommended)."
|
|
const src = code('autorun.brs');
|
|
assert.ok(!/storage_quota:\s*"/.test(src), 'storage_quota should be a double, not a string');
|
|
assert.match(src, /storage_quota:\s*\d+\.\d/, 'storage_quota must be a double literal');
|
|
});
|
|
|
|
test('nodejs_enabled is what gates require("@brightsign/*")', () => {
|
|
// The bridge is entirely require()-based. brightsign_js_objects_enabled gates the LEGACY GLOBALS
|
|
// (BSDeviceInfo, BSMessagePort) and not the modules — BrightSign's own cookbook calls
|
|
// require("@brightsign/bt") with nodejs_enabled alone. Losing nodejs_enabled costs the player its
|
|
// identity and its restart delegation, silently.
|
|
const src = code('autorun.brs');
|
|
assert.match(src, /nodejs_enabled:\s*true/, 'without this there are no @brightsign modules at all');
|
|
});
|
|
|
|
test('a second widget is never given the first screen\'s rectangle', () => {
|
|
// roHtmlWidget has NO output selector — not in its init parameters, not in the JavaScript
|
|
// HtmlWidgetParams. A second output is addressed by building one tall canvas with SetScreenModes
|
|
// and placing the widget at that output's display_x/display_y. Reusing the full-screen rect puts
|
|
// both widgets on output ONE, on top of each other, while output two stays dark.
|
|
const src = code('autorun.brs');
|
|
if (!/widget2\s*=\s*MakeWidget/.test(src)) return;
|
|
// Split on TOP-LEVEL commas: the first argument is itself a call (PlayerUrl(cfg, screen2)), and a
|
|
// naive split lands on its inner comma and inspects the wrong argument — which is how the first
|
|
// draft of this rule passed against the very source it was written to reject.
|
|
const call = /widget2\s*=\s*MakeWidget\((.*)\)\s*$/m.exec(src);
|
|
assert.ok(call, 'could not find the second widget');
|
|
const args = [];
|
|
let depth = 0;
|
|
let cur = '';
|
|
for (const ch of call[1]) {
|
|
if (ch === '(') depth++;
|
|
if (ch === ')') depth--;
|
|
if (ch === ',' && depth === 0) { args.push(cur.trim()); cur = ''; continue; }
|
|
cur += ch;
|
|
}
|
|
args.push(cur.trim());
|
|
assert.equal(args.length, 4, `unexpected MakeWidget arity: ${call[0]}`);
|
|
assert.notEqual(args[1], 'rect',
|
|
'the second widget must be positioned at the second output\'s canvas offset, not at rect');
|
|
});
|