v3.23.4: the event form's numeric option values (v3.22.0 regression)

esc() is string-only, and most EVENT_FORMS option values are numbers
({v:0,l:'Normal service'}, the ACCESSTECH list, ...): the shared event-form
renderer escaped them with esc(o.v), so opening any such event form threw
'str.replace is not a function' - in the Phone tab (pysimEventSendForm) and
the test-script step editor alike (both call the same builder).  Introduced
with v3.22.0, when the event action started using the Simulator's forms.

Option values (and the data-gr group) are stringified before escaping, the
pattern the sibling option builder already used; the emitted DOM values are
unchanged ("0", "10").

Test: event_forms.test.js renders every event form (Phone and step-editor
variants) and asserts a numeric option prefills selected - verified to fail
on the pre-fix code.  848 frontend + 830 Python; sw cache simple-v417.
This commit is contained in:
2026-10-07 13:44:31 +03:00
parent e8c991648d
commit cc335def00
5 changed files with 37 additions and 6 deletions
+31
View File
@@ -38,6 +38,8 @@ eval(extractBlock('const EVENT_INNER_MAX = ', 'function pysimEventSendForm').rep
eval(extractFunc(html, 'eventFieldsForRelease'));
eval(extractFunc(html, 'encPlmn'));
eval(extractFunc(html, 'bytesToHex'));
eval(extractFunc(html, 'esc'));
eval(extractFunc(html, 'eventFormFieldsHtml'));
test('CMD_NAMES decodes timer management and the BIP commands', () => {
assert.strictEqual(CMD_NAMES['01'], 'REFRESH');
@@ -344,3 +346,32 @@ test('the shared event vectors match the PWA builders', () => {
'scts ' + JSON.stringify(v));
}
});
test('every event form renders - numeric option values are stringified (v3.23.4)', () => {
// esc() is string-only and most EVENT_FORMS option values are numbers
// (e.g. {v:0,l:'Normal service'} and the ACCESSTECH list): rendering the
// form with esc(o.v) crashed with 'str.replace is not a function' for the
// Phone tab and the test-script step editor alike (v3.22.0 regression).
globalThis.t = s => s;
const bad = [];
for (const [ev, cfg] of Object.entries(EVENT_FORMS)) {
if (!cfg) continue; // the unimplemented events carry a null form
for (const withOnchange of [true, false]) {
try {
eventFormFieldsHtml(cfg, cfg.fields, {}, 'x-', withOnchange);
} catch (e) {
bad.push(ev + (withOnchange ? '/phone' : '/step') + ': ' + e.message);
}
}
}
assert.deepStrictEqual(bad, [], 'every event form must render');
// a numeric option value lands as its string form and prefills selected
const cfg = EVENT_FORMS[0x00];
const sel = cfg.fields.find(f => f.type === 'select' && f.opts.some(o => typeof o.v === 'number'));
assert.ok(sel, 'a numeric-option select must exist');
const num = sel.opts.find(o => typeof o.v === 'number');
const out = eventFormFieldsHtml(cfg, cfg.fields, {[sel.id]: num.v}, 'x-', true);
assert.ok(out.includes('value="' + String(num.v) + '"'), out);
assert.ok(out.includes('value="' + String(num.v) + '" selected'), out);
delete globalThis.t;
});