EF.SST: tri-state editor (deleted / allocated-off / on)

Give EF.SST its own per-file editor for the 2-bit SIM service table
(TS 51.011 10.3.7: allocated + activated per service):

- efEncSst encodes the pair layout, preserving unmodelled pairs and the
  file length; validates service number, fit and state.
- A new `bits2` field kind renders a three-state select per service and
  collects {n, name, allocated, activated}; registered as EF_SST_FIELDS
  with the encoder (df '7F20'), and SST leaves EF_READONLY_NAMES.
- i18n: Deleted/Off/On + the state-error and hint strings (EN/RU).

Docs/help updated: SST is no longer read-only.  Frontend 1093 + Python
1090 tests green.  (Version/sw bump still deferred for the unreleased
decoded-editing batch.)
This commit is contained in:
catarrh committed 2026-10-25 21:43:07 +03:00
1 parent 5ebcd0773d
commit fc27feba8e
6 files changed
+296 -30

No files matched your search

+76 -8
View File
@@ -51,13 +51,13 @@ const FNS = [
'efEncPlmnList', 'efListRows', 'efPlmnRowOrig', 'efEncAdn', 'efEncAdnExt', 'efPnnNameBytes', 'efPnnHexBytes', 'efPnnOrigTlv', 'efEncPnn',
'efFindEncoder', 'efHasEncoder', 'gsm7Decode', 'gsm7TextToSeptets',
'gsm7Encode', 'efEncError', 'efDecModelValues',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncSst', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_SST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS']) {
code += extractConst(html, c);
}
eval(code);
@@ -233,12 +233,19 @@ test('the encoder resolver is DF-first: every EF.AD form shares one encoder', ()
// without a DF the historical name-first lookup answers (DF.GSM's entry)
assert.strictEqual(efFindEncoder('EF.AD', '6fad'), sim);
assert.strictEqual(efFindEncoder('EF.AD', '6fad', 'ADF.NOPE'), sim);
// EF.ECC stays read-only in either DF, EF.SST in the SIM layout, and the
// shared 6F38 FID must not hand SST the UST encoder
// EF.ECC stays read-only in either DF; the shared 6F38 FID resolves to
// EF.SST under DF.GSM and to EF.UST under ADF.USIM (the FID fallback
// without a DF keeps the historical UST answer - SST's entry is DF-scoped)
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', '7F20'), false);
assert.strictEqual(efHasEncoder('EF.ECC', '6fb7', 'ADF.USIM'), false);
assert.strictEqual(efHasEncoder('EF.SST', '6f38', '7F20'), false);
assert.strictEqual(efHasEncoder('EF.SST', '6f38', '7F20'), true);
assert.strictEqual(efHasEncoder('EF.UST', '6f38', 'ADF.USIM'), true);
assert.strictEqual(efFindEncoder('EF.SST', '6f38', '7F20').enc, efEncSst);
assert.strictEqual(efFindEncoder('EF.UST', '6f38', 'ADF.USIM').enc, efFindEncoder('EF.UST', '6f38').enc);
assert.strictEqual(efFindEncoder(null, '6f38', '7F20').name, 'EF.SST');
assert.strictEqual(efFindEncoder(null, '6f38', 'ADF.USIM').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38', 'ADF.USIM').enc !== efEncSst, true);
assert.strictEqual(efFindEncoder('EF.NOPE', 'abcd', 'ADF.USIM'), null);
// the SIM 3+X and USIM 4+X forms keep their length on an untouched save
for (const hex of ['00' + '0000', '00' + '0000' + '02' + 'AABB', AD_HEX, 'FF'.repeat(6)]) {
@@ -626,10 +633,13 @@ test('service tables: encode/decode round-trips and unmodelled bits', () => {
const services = efDecServiceTable(efBytes(hex), table).services;
assert.strictEqual(entry.enc({ services: services }, hex), hex, name + ' ' + hex);
}
// EF.SST has no editor (its 2-bit SIM layout needs a dedicated UI)
assert.strictEqual(efFindEncoder('EF.SST', '6f38'), null);
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), false);
// EF.SST has its own 2-bit editor on the shared 6F38 FID (the DF-scoped
// entry keeps the UST answer for a bare FID / ADF context)
assert.strictEqual(efFindEncoder('EF.SST', '6f38').enc, efEncSst);
assert.strictEqual(efHasEncoder('EF.SST', '6f38'), true);
assert.strictEqual(efFindEncoder('EF.UST', '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38').name, 'EF.UST');
assert.strictEqual(efFindEncoder(null, '6f38', '7F20').name, 'EF.SST');
// EST defines services 1-3; all-FF keeps the other bits too
assert.deepStrictEqual(efDecServiceTable(efBytes('07'), EF_EST_SERVICES).services,
[{ n: 1, name: 'Fixed Dialling Numbers (FDN)' },
@@ -659,6 +669,64 @@ test('service tables: unknown services and a too-short table are refused', () =>
assert.throws(() => efEncServiceTable({ services: [{ n: 0 }] }, '00', EF_UST_SERVICES), /Unknown service/);
});
// EF.SST (TS 51.011 10.3.7): two bits per service, four per byte. 15 bytes =
// services 1-60; C7/1C/C1 carry on (11), off (10) and deleted (00) pairs and
// the last byte's bits 7-8 are service 60, above the 59-entry table.
const SST_MIX = 'C71CC1' + '00'.repeat(11) + 'C0';
test('EF.SST: the 2-bit encoder round-trips the decoded file', () => {
const dec = efDecSst(efBytes(SST_MIX));
assert.deepStrictEqual(dec.services.map(s => [s.n, s.allocated, s.activated]), [
[1, true, true], [2, true, false], [4, true, true], [6, true, true],
[7, true, false], [9, true, false], [12, true, true], [60, true, true],
]);
// a deleted (00) pair is omitted by the decoder, so decode->encode is exact
assert.strictEqual(efEncSst(dec, SST_MIX), SST_MIX);
// every state on: all-FF, unmodelled service 60 included
assert.strictEqual(efEncSst(efDecSst(efBytes('FF'.repeat(15))), 'FF'.repeat(15)), 'FF'.repeat(15));
// no service allocated at all
assert.strictEqual(efEncSst(efDecSst(efBytes('00'.repeat(15))), '00'.repeat(15)), '00'.repeat(15));
// clearing every modelled service keeps service 60's pair (byte 14 C0)
assert.strictEqual(efEncSst({ services: [] }, SST_MIX), '000000' + '00'.repeat(11) + 'C0');
// the file length is kept
assert.strictEqual(efEncSst(efDecSst(efBytes('0C')), '0C'), '0C');
assert.strictEqual(efEncSst(efDecSst(efBytes('0C00')), '0C00'), '0C00');
});
test('EF.SST: deleted clears the pair, off sets 10 and on sets 11', () => {
const on = n => ({ n: n, allocated: true, activated: true });
const off = n => ({ n: n, allocated: true, activated: false });
const del = n => ({ n: n, allocated: false, activated: false });
// service 2 (byte 0, pair 2)
assert.strictEqual(efEncSst({ services: [on(2)] }, '00'), '0C');
assert.strictEqual(efEncSst({ services: [off(2)] }, '00'), '04');
assert.strictEqual(efEncSst({ services: [del(2)] }, '0C'), '00');
// every modelled pair is rebuilt from the list, the rest of the file stays
assert.strictEqual(efEncSst({ services: [on(2)] }, 'FF'.repeat(15)),
'0C' + '00'.repeat(13) + 'C0');
assert.strictEqual(efEncSst({ services: [on(2), off(4)] }, SST_MIX).slice(0, 2), '4C');
// a pair above the table is written like any other (a longer file)
const long = '00'.repeat(16);
assert.strictEqual(efEncSst({ services: [on(62)] }, long), '00'.repeat(15) + '0C');
// the decoder's illegal '08' pair (activated, not allocated) is refused
assert.deepStrictEqual(efDecSst(efBytes('08')), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: false, activated: true },
] });
assert.throws(() => efEncSst(efDecSst(efBytes('08')), '08'), /must be deleted, off or on/);
});
test('EF.SST: unknown services, an invalid state and a too-short file are refused', () => {
assert.throws(() => efEncSst({ services: [{ n: 5, allocated: true, activated: true }] }, '00'),
/too short for service/);
assert.throws(() => efEncSst({ services: [{ n: 0, allocated: true, activated: true }] }, '00'),
/Unknown service/);
assert.throws(() => efEncSst({ services: [{ n: 'x' }] }, '00'), /Unknown service/);
assert.throws(() => efEncSst({ services: [{ n: 2, allocated: false, activated: true }] }, '00'),
/must be deleted, off or on/);
// a bare service number carries no state
assert.throws(() => efEncSst({ services: [2] }, '00'), /must be deleted, off or on/);
});
test('F-ACC: the class numbering and the ACC10/encoder rules', () => {
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [15] });
assert.deepStrictEqual(efDecAcc(efBytes('0001')), { access_classes: [0] });
+95 -8
View File
@@ -51,19 +51,19 @@ const FNS = [
'efDecRowHtml', 'efDecGridHtml', 'efDecItemRowHtml', 'efDecItemsHtml',
'efDecRecordsHtml', 'efDecEditHtml',
'efDecCollectValues', 'efDecCollectRows', 'efDecCollectItems',
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
'efBitsSelected', 'pysimFsDecBitsFilter', 'pysimFsDecBits2Filter', 'pysimFsDecRowCheck', 'pysimFsDecItemTag', 'pysimFsDecAddItem',
'pysimFsDecRemoveItem', 'pysimFsFindNodeByPath', 'pysimCustomNormPath', 'pysimFsNodePath', 'pysimFsFallbackNode', 'efFidFromPath', 'efDfFromPath',
'pysimFsHasDecoder', 'pysimFsHasEncoder', 'pysimFsDecSave', 'pysimFsDecEdit',
'pysimFsPillsEnabled', 'pysimFsSyncContentVisibility', 'gsm7Decode',
'gsm7TextToSeptets', 'gsm7Encode',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
'efDecServiceTable', 'efDecSst', 'efDecAcc', 'efDecPhase', 'efDecHpplmn', 'efDecAd', 'efDecCbmi', 'efDecCbmir', 'efDecIccid', 'efDecImsi', 'efDecPlmnList', 'efDecFplmn', 'efEncServiceTable', 'efEncSst', 'efEncAcc', 'efPhaseValue', 'efPhaseModel', 'efEncPhase', 'efEncHpplmn', 'efAdOpValue', 'efAdModel', 'efEncAd', 'efEncPlmnOnly', 'efEncIccid', 'efEncImsi', 'efCbmiModel', 'efEncCbmi', 'efCbmirModel', 'efEncCbmir', 'efSmspAddressModel', 'efSmspModel', 'efSmspTonOptions', 'efSmspParamRow', 'efSmspHtml', 'efSmspCollect', 'pysimFsSmspToggle', 'pysimFsSmspHexInput', 'efSmspHexByte', 'efSmspAddressBytes', 'efSmspAddrUnchanged', 'efSmspByteUnchanged', 'efEncSmsp', 'smspT', 'smspDcsHint', 'smspVpHint', 'smspDurationText', 'smspPluralIndex', 'smspCount', 'efSmspDaNumber',
];
let code = 'var pysimFsSelected = null;\nvar pysimFsTreeRoot = null;\nvar pysimFsDecModel = null;\n'
+ 'var pysimFsDecEditMode = false;\nvar pysimFsEditMode = false;\n';
for (const f of FNS) code += extractFunc(html, f) + '\n';
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'EF_ACT_TECHS', 'EF_SPN_FIELDS',
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS', 'EF_ADF_RIDS']) {
'EF_ADN_FIELDS', 'EF_PLMN_LIST', 'EF_PNN_ITEMS', 'EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_EST_SERVICES', 'EF_PHASE_NAMES', 'EF_ACC_CLASSES', 'EF_UST_FIELDS', 'EF_IST_FIELDS', 'EF_EST_FIELDS', 'EF_SST_FIELDS', 'EF_ACC_FIELDS', 'EF_PHASE_FIELDS', 'EF_HPPLMN_FIELDS', 'EF_AD_OP_MODES', 'EF_AD_FIELDS', 'EF_PLMN_ONLY_LIST', 'EF_ICCID_FIELDS', 'EF_IMSI_FIELDS', 'EF_CBMI_LIST', 'EF_CBMIR_LIST', 'EF_READONLY_NAMES', 'EF_SMSP_PARAMS', 'EF_SMSP_CUSTOM', 'EF_ENCODERS', 'EF_ADF_RIDS']) {
code += extractConst(html, c);
}
eval(code);
@@ -271,12 +271,14 @@ test('pysimFsHasEncoder resolves the selected file by its path or FID', () => {
assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = 'MF/6F7E';
assert.strictEqual(pysimFsHasEncoder(), false);
// the SIM service table shares 6F38 with UST but has no editor
// the SIM service table shares 6F38 with UST but has its own 2-bit editor
pysimFsTreeRoot = { name: 'MF', children: [] };
const sst = { name: 'EF.SST', fid: '6f38', parent: pysimFsTreeRoot };
pysimFsTreeRoot.children.push(sst);
pysimFsSelected = 'MF/6F38';
assert.strictEqual(pysimFsHasEncoder(), false);
const gsm = { name: 'DF.GSM', fid: '7F20', parent: pysimFsTreeRoot, children: [] };
pysimFsTreeRoot.children.push(gsm);
const sst = { name: 'EF.SST', fid: '6f38', parent: gsm };
gsm.children.push(sst);
pysimFsSelected = 'MF/7F20/6F38';
assert.strictEqual(pysimFsHasEncoder(), true);
pysimFsSelected = null;
assert.strictEqual(pysimFsHasEncoder(), false);
});
@@ -749,6 +751,91 @@ test('the bits filter hides the non-matching rows', () => {
assert.strictEqual(rows[1].style.display, '');
});
test('the EF.SST grid renders one tri-state select per service', () => {
// 4C: service 2 on (11), service 4 off (10), the rest of the pairs deleted
const model = efDecEditModel('EF.SST', '6f38', { kind: 'transparent', data: '4C' }, '7F20');
assert.deepStrictEqual(model.values.services, [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: false },
]);
const out = efDecEditHtml(model, '4C');
assert.match(out, /class="pysim-dec-bits /);
assert.match(out, /pysim-dec-bits-filter/);
assert.match(out, /data-bit2="2" data-bit2-name="Abbreviated Dialling Numbers \(ADN\)"/);
// the whole SST table is rendered (59 services, RFU 8/20/50 included)
assert.strictEqual((out.match(/data-bit2="/g) || []).length, 59);
assert.match(out, /data-bit2="8" data-bit2-name="RFU"/);
assert.match(out, /data-bit2="20" data-bit2-name="RFU"/);
assert.match(out, /data-bit2="50" data-bit2-name="Reserved and shall be ignored"/);
// each select is preselected from the decoded state, an absent pair is deleted
const sel = n => {
const m = new RegExp('<select data-bit2="' + n + '"[^>]*>([\\s\\S]*?)</select>').exec(out);
assert.ok(m, 'select for service ' + n);
return m[1];
};
assert.match(sel(2), /<option value="on" selected>On<\/option>/);
assert.match(sel(4), /<option value="off" selected>Off<\/option>/);
assert.match(sel(1), /<option value="deleted" selected>Deleted<\/option>/);
assert.match(sel(59), /<option value="deleted" selected>Deleted<\/option>/);
for (const n of [2, 4, 1, 59]) {
assert.match(sel(n), />Deleted<\/option>/);
assert.match(sel(n), />Off<\/option>/);
assert.match(sel(n), />On<\/option>/);
}
// an all-FF file preselects every modelled service 'on'
const ff = efDecEditHtml(efDecEditModel('EF.SST', '6f38',
{ kind: 'transparent', data: 'FF'.repeat(15) }, '7F20'), 'FF'.repeat(15));
assert.strictEqual((ff.match(/<option value="on" selected>/g) || []).length, 59);
assert.ok(!/<option value="(deleted|off)" selected>/.test(ff));
});
test('the EF.SST collector yields {n, name, allocated, activated}', () => {
const row = (n, name, state) => ({
value: state,
getAttribute: k => k === 'data-bit2' ? String(n) : name,
});
const root = {
querySelectorAll: sel => sel === '[data-bit2]' ? [
row(2, 'Abbreviated Dialling Numbers (ADN)', 'on'),
row(1, 'CHV1 disable function', 'deleted'),
row(4, 'Short Message Storage (SMS)', 'off'),
] : [],
};
assert.deepStrictEqual(efDecCollectValues(root, EF_SST_FIELDS), { services: [
{ n: 2, name: 'Abbreviated Dialling Numbers (ADN)', allocated: true, activated: true },
{ n: 1, name: 'CHV1 disable function', allocated: false, activated: false },
{ n: 4, name: 'Short Message Storage (SMS)', allocated: true, activated: false },
] });
// an untouched form re-encodes to the original bytes
const dec = efDecSst(efBytes('4C'));
const values = efDecCollectValues({
querySelectorAll: () => [2, 4].map(n => row(n, EF_SST_SERVICES[n],
dec.services.find(s => s.n === n).activated ? 'on' : 'off')),
}, EF_SST_FIELDS);
assert.strictEqual(efFindEncoder('EF.SST', '6f38', '7F20').enc(values, '4C'), '4C');
// the EF.SST field is the tri-state kind, the 1-bit tables are untouched
assert.strictEqual(EF_SST_FIELDS[0].kind, 'bits2');
assert.strictEqual(EF_UST_FIELDS[0].kind, 'bits');
assert.strictEqual(EF_EST_FIELDS[0].kind, 'bits');
});
test('the tri-state grid filter matches the row label, not the select options', () => {
const rows = [
{ getAttribute: k => k === 'data-label' ? '1 CHV1 disable function' : '', style: {} },
{ getAttribute: k => k === 'data-label' ? '4 Short Message Storage (SMS)' : '', style: {} },
];
const box = { querySelectorAll: sel => sel === '.pysim-dec-bit2-row' ? rows : [] };
const input = { value: 'sms', closest: sel => sel === '.pysim-dec-bits' ? box : null };
pysimFsDecBits2Filter(input);
assert.strictEqual(rows[0].style.display, 'none');
assert.strictEqual(rows[1].style.display, '');
// 'off' is a state option of every row, never part of a service name
pysimFsDecBits2Filter({ value: 'off', closest: () => box });
assert.strictEqual(rows[0].style.display, 'none');
assert.strictEqual(rows[1].style.display, 'none');
pysimFsDecBits2Filter(null);
});
test('the select keeps an unknown current value as an option', () => {
const model = efDecEditModel('EF.PHASE', '6fae', { kind: 'transparent', data: '0F' });
assert.deepStrictEqual(model.values, { phase_hex: '0F' });