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:
1 parent
5ebcd0773d
commit
fc27feba8e
6 files changed
+296
-30
No files matched your search
@@ -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' });
|
||||
|
||||
Reference in new issue
Block a user