c69a779ca4
The access-rules block was read-only and its Check radio was disabled without captured lines, so a hand-made rule could not define rules and captured ones could not be adjusted. It now edits the decoded rule's structure: - fcpArrRuleParse/fcpArrRuleEncode parse a canonical decoded line into the AM (bit mask, RESIZE FILE, INCREASE) + SC condition model and re-encode it byte-identically (round-trip tested); the template forms (OR/AND/NOT, SM CRTs, command descriptions) stay raw and removable. - The editor renders each rule as operation checkboxes (or the command form) and condition selects (always / never / a key reference with its usage), with a preview line of the canonical string the check compares; + rule adds READ always; + condition / x adjust the rows. - The None/Check radio no longer disables; a new rule can be built by hand and captured rules adjusted. The stored format is unchanged (the canonical strings), so capture, the check, the set comparison and validation are untouched. Tests: parse/encode round-trips (incl. raw forms), the mutators, the editor render, plus the existing check tests. 882 frontend + 858 Python; sw cache simple-v424.
329 lines
18 KiB
JavaScript
329 lines
18 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
|
|
|
function extractFunc(src, name) {
|
|
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
|
const m = re.exec(src);
|
|
if (!m) throw new Error('function ' + name + ' not found');
|
|
let i = m.index + m[0].length - 1;
|
|
let depth = 0;
|
|
for (; i < src.length; i++) {
|
|
if (src[i] === '{') depth++;
|
|
else if (src[i] === '}') {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
}
|
|
return src.slice(m.index, i + 1);
|
|
}
|
|
|
|
const FNS = [
|
|
'efBytes', 'efHex', 'efSwapNibbles', 'efAllFf', 'efRstripFf', 'efTlv', 'efUcs2Be',
|
|
'efGsm7Octets', 'efAnnexA', 'efBcdAddress', 'efBcdAddressLen', 'efPlmn', 'efPlmnAct', 'efPnnText',
|
|
'efDecIccid', 'efDecImsi', 'efDecLi', 'efDecServiceTable', 'efDecPlmnList', 'efDecFplmn', 'efDecPlmnWact',
|
|
'efDecSpn', 'efDecLoci', 'efDecPsLoci', 'efDecEpsLoci', 'efDecEpsNsc', 'efDecKc', 'efDecAcc',
|
|
'efDecPhase', 'efDecCbmi', 'efDecCbmir', 'efDecEcc', 'efDecOpl', 'efDecAd', 'efDecAcl',
|
|
'efDecSmsp', 'efDecSpdi', 'efDecNai', 'efDecDir', 'efDecArr', 'efDecPnn', 'efDecAdn',
|
|
'pinKeyRefName', 'efArrAmLabel', 'efArrSc', 'efArrAmDo',
|
|
'efDecExt1', 'efDecSms', 'efDecSume', 'efFindDecoder', 'efFidFromPath', 'efDecodeBytes',
|
|
'efDecodeFile', 'efDecodeHex', 'efIsRawOnly', 'efFieldLabel', 'efPrimitive', 'efFlatten',
|
|
'efDiffData', 'efDataSummary', 'efContentDiff', 'gsm7Decode', 'efRenderFieldsHtml',
|
|
'pysimFsDecodedHtml', 'profilerRenderReport', 'profilerLabelText', 'profilerResultAspects',
|
|
'profilerAspectSummary', 'profilerMatchedRecordsText', 'profilerRawDataCheck', 'profilerCustomNameForPath',
|
|
'pysimCustomNormPath',
|
|
];
|
|
|
|
let code = '';
|
|
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
|
for (const c of ['GSM7_ALPHABET', 'GSM7_EXT_MAP', 'FCP_ARR_AM_BITS']) {
|
|
code += html.match(new RegExp('const ' + c + ' = [\\s\\S]*?\\n\\];'))[0].replace('const ', 'var ') + '\n';
|
|
}
|
|
for (const c of ['EF_UST_SERVICES', 'EF_SST_SERVICES', 'EF_IST_SERVICES', 'EF_PHASE_NAMES']) {
|
|
code += html.match(new RegExp('const ' + c + ' = \\{.*\\};'))[0].replace('const ', 'var ') + '\n';
|
|
}
|
|
code += html.match(/const EF_FIELD_LABELS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n';
|
|
code += html.match(/const EF_DECODERS = \[[\s\S]*?\n\];/)[0].replace('const ', 'var ') + '\n';
|
|
code += 'var pysimCustomFiles = [];\nvar pysimFsSelected = \'EF.IMSI\';\n';
|
|
code += 'function esc(s){return String(s);}\nfunction escHtml(s){return String(s);}\nfunction t(k){return k;}\n';
|
|
eval(code);
|
|
|
|
test('ef helpers: bytes, hex, swap, TLV, PLMN', () => {
|
|
assert.strictEqual(efHex(efBytes('a0ff01')), 'A0FF01');
|
|
assert.strictEqual(efSwapNibbles('1234'), '2143');
|
|
assert.strictEqual(efPlmn(efBytes('22F860')).mcc, '228');
|
|
assert.strictEqual(efPlmn(efBytes('22F860')).mnc, '06');
|
|
assert.ok(efAllFf(efBytes('FFFF')));
|
|
assert.ok(!efAllFf(efBytes('FF00')));
|
|
// 05 03 AA BB CC + 1E 01 DD
|
|
const tlvs = efTlv(efBytes('0503AABBCC1E01DD'));
|
|
assert.deepStrictEqual(tlvs.map(t => t.tag), [0x05, 0x1E]);
|
|
assert.strictEqual(efHex(tlvs[0].value), 'AABBCC');
|
|
assert.strictEqual(efHex(tlvs[1].value), 'DD');
|
|
// long-form length
|
|
assert.strictEqual(efTlv(efBytes('058105' + '00'.repeat(5)))[0].value.length, 5);
|
|
});
|
|
|
|
test('efAnnexA decodes GSM7 octets, UCS2 and packed variants', () => {
|
|
assert.strictEqual(efAnnexA(efBytes('47534D2D52204348')), 'GSM-R CH'); // one octet per char
|
|
assert.strictEqual(efAnnexA(efBytes('8000470053004D')), 'GSM'); // UCS2 prefix
|
|
assert.strictEqual(efAnnexA(efBytes('')), '');
|
|
assert.strictEqual(efAnnexA(efBytes('FFFFFFFF')), '');
|
|
});
|
|
|
|
test('efPnnText uses the spare-bit count (TS 24.008 10.5.3.5a)', () => {
|
|
// Live card EF.PNN records: coding octet + packed GSM-7 text. The spare
|
|
// bits in the coding octet give the exact character count, so the zero
|
|
// padding must not decode as a trailing '@' (record 2 was 'Miranda@').
|
|
const cases = [
|
|
['8441b6390c', 'Alfa'],
|
|
['87cdb43cec268701', 'Miranda'],
|
|
['83d7b41b', 'Win'],
|
|
['83cdb41c442db3cbeb771b', 'Mir Telekom'],
|
|
['82ab1b885a6697d7ef36', '+7 Telekom'],
|
|
['85c6b23b8d07', 'Fenix'],
|
|
['83cde514', 'MKS'],
|
|
];
|
|
for (const [hex, text] of cases) {
|
|
assert.strictEqual(efPnnText(efBytes(hex)), text, hex);
|
|
}
|
|
// UCS2 coding scheme (octet 3 bits 5-7 = 001)
|
|
assert.strictEqual(efPnnText(efBytes('91004d006900720061006e00640061')), 'Miranda');
|
|
// a final <CR> used as padding is removed (TS 23.038 6.1.2.1.1)
|
|
assert.strictEqual(gsm7Decode(efBytes('cdb43cec26871b'), 8), 'Miranda');
|
|
});
|
|
|
|
test('EF decoders match the pinned spec / pySim test vectors', () => {
|
|
assert.deepStrictEqual(efDecIccid(efBytes('988812010000400310f0')), { iccid: '8988211000000430010' });
|
|
assert.deepStrictEqual(efDecImsi(efBytes('082982608200002080')), { imsi: '228062800000208' });
|
|
assert.deepStrictEqual(efDecLi(efBytes('6465')), { languages: ['de'] });
|
|
assert.deepStrictEqual(efDecLi(efBytes('FFFF')), { languages: [] });
|
|
assert.deepStrictEqual(efDecSpn(efBytes('0147534d2d52204348ffffffffffffffff')), { show_in_hplmn: true, hide_in_oplmn: false, name: 'GSM-R CH' });
|
|
assert.deepStrictEqual(efDecAcc(efBytes('0000')), { access_classes: [] });
|
|
assert.deepStrictEqual(efDecAcc(efBytes('8000')), { access_classes: [0] });
|
|
assert.deepStrictEqual(efDecEcc(efBytes('19f101')), { number: '911', esc: '0x01' });
|
|
assert.deepStrictEqual(efDecKc(efBytes('837d783609a3858f05')), { kc: '837D783609A3858F', ksi: 0 });
|
|
assert.deepStrictEqual(efDecPlmnList(efBytes('22F860')), { plmns: [{ mcc: '228', mnc: '06' }] });
|
|
// EF.FPLMN: a FFFFFF gap in any position is skipped, not a terminator
|
|
assert.deepStrictEqual(efDecFplmn(efBytes('22F860FFFFFF62F210')),
|
|
{ plmns: [{ mcc: '228', mnc: '06' }, { mcc: '262', mnc: '01' }] });
|
|
assert.deepStrictEqual(efDecFplmn(efBytes('FFFFFFFF')), { plmns: [] });
|
|
assert.strictEqual(efFindDecoder('EF.EHPLMN', null).fn, efDecPlmnList);
|
|
assert.strictEqual(efFindDecoder('EF.FPLMN', null).fn, efDecFplmn);
|
|
assert.deepStrictEqual(efDecCbmi(efBytes('0010FFFF0020')), { message_ids: [16, 32] });
|
|
assert.deepStrictEqual(efDecCbmir(efBytes('0000FFFEFFFF')), { ranges: [['0000', 'FFFE']] });
|
|
assert.deepStrictEqual(efDecPhase(efBytes('03')), { phase: 'phase 2 and higher' });
|
|
assert.deepStrictEqual(efDecNai(efBytes('8021696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267')),
|
|
{ text: 'ims.mnc000.mcc738.3gppnetwork.org' });
|
|
assert.deepStrictEqual(efDecImsi(efBytes('FFFF')), { empty: true });
|
|
assert.deepStrictEqual(efDecIccid(efBytes('FFFFFFFFFFFF')), { empty: true });
|
|
});
|
|
|
|
test('EF.SUME decodes the SET UP MENU title and icon TLVs (TS 51.011 10.3.34)', () => {
|
|
// 05 05 "Menu!" + 1E 02 01 23 + FF padding
|
|
const d = efDecSume(efBytes('05054D656E75211E020123FFFFFFFF'));
|
|
assert.deepStrictEqual(d, { title: 'Menu!', icon: '0123' });
|
|
assert.deepStrictEqual(efDecSume(efBytes('FFFFFFFF')), { empty: true });
|
|
// UCS2 title: 05 09 80 00 4D 00 65 00 6E 00 75
|
|
assert.deepStrictEqual(efDecSume(efBytes('050980004D0065006E0075FFFFFFFF')),
|
|
{ title: 'Menu' });
|
|
});
|
|
|
|
test('ADN-format records: alpha + BCD number + CCP/EXT1', () => {
|
|
const d = efDecAdn(efBytes('42204841203120536963FFFFFFFFFFFF06810628560810FFFFFFFFFFFFFF'));
|
|
assert.strictEqual(d.name, 'B HA 1 Sic');
|
|
assert.strictEqual(d.number, '6082658001');
|
|
assert.strictEqual(d.ccp, undefined);
|
|
assert.strictEqual(d.ext1, undefined);
|
|
// international number in EF.MSISDN
|
|
const m = efDecAdn(efBytes('ffffffffffffffffffffffffffffffffffffffff04b12143f5ffffffffffffffffff'));
|
|
assert.strictEqual(m.name, '');
|
|
assert.strictEqual(m.number, '12345');
|
|
// EXT1 per TS 51.011 10.5.10: type 02 (additional data) + count 03 + BCD
|
|
assert.deepStrictEqual(efDecExt1(efBytes('02032143F5' + 'FF'.repeat(8))), { number: '12345' });
|
|
});
|
|
|
|
test('EF record decoders: SMS, OPL, DIR, ARR, IMPI/SMSP', () => {
|
|
const opl = efDecOpl(efBytes('62f2100000fffe01'));
|
|
assert.deepStrictEqual(opl, { mcc: '262', mnc: '01', lac_range: ['0000', 'FFFE'], pnn_record: 1 });
|
|
const dir = efDecDir(efBytes('61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150'));
|
|
assert.strictEqual(dir.applications.length, 1);
|
|
assert.strictEqual(dir.applications[0].aid, 'A0000000871002FFFFFFFF8907090000');
|
|
assert.strictEqual(dir.applications[0].label, 'USim1');
|
|
// EF.ARR: AM_DO 01 (read) guarded by ADM1 (TS 102 221 9.2.7 / ISO 7816-4 5.4.3.2)
|
|
const arr = efDecArr(efBytes('800101a40683010a950108'));
|
|
assert.deepStrictEqual(arr.rules, ['AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): ADM1 (verify)']);
|
|
// Two rules: update needs PIN app1 or app2, read is always
|
|
assert.deepStrictEqual(
|
|
efDecArr(efBytes('800102a010a406830101950108a4068301029501088001019000')).rules,
|
|
['AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): (PIN app1 (verify) OR PIN app2 (verify))',
|
|
'AM 0x01 (READ/SEARCH (EF) / DELETE FILE child (DF)): always']);
|
|
// INCREASE / RESIZE ride AM_DO 0x84 with the command INS as value
|
|
assert.deepStrictEqual(efDecArr(efBytes('8401329000')).rules,
|
|
['INCREASE (AM_DO 0x84, INS 0x32): always']);
|
|
assert.deepStrictEqual(efDecArr(efBytes('8401D49000')).rules,
|
|
['RESIZE FILE (AM_DO 0x84, INS 0xD4): always']);
|
|
// b8=1: bits 7-4 are proprietary
|
|
assert.deepStrictEqual(efDecArr(efBytes('8001819000')).rules,
|
|
['AM 0x81 (bits 7-4 proprietary + READ/SEARCH (EF) / DELETE FILE child (DF)): always']);
|
|
const impi = efDecNai(efBytes('803137333830303630303030303031303140696d732e6d6e633030302e6d63633733382e336770706e6574776f726b2e6f7267'));
|
|
assert.strictEqual(impi.text, '738006000000101@ims.mnc000.mcc738.3gppnetwork.org');
|
|
const smsp = efDecSmsp(efBytes('534d5343ffffffffffffffffffffffffe1ffffffffffffffffffffffff0891945197109099f9ffffff0000a9'));
|
|
assert.strictEqual(smsp.alpha, 'SMSC');
|
|
assert.strictEqual(smsp.sca, '+4915790109999');
|
|
assert.strictEqual(smsp.tp_pid, '0x00');
|
|
assert.strictEqual(smsp.tp_dcs, '0x00');
|
|
// EF.SMS status byte + skip SMSC + TPDU
|
|
const sms = efDecSms(efBytes('030891945197109099f90000'));
|
|
assert.strictEqual(sms.direction, 'MT');
|
|
assert.strictEqual(sms.status, 'message to be read');
|
|
assert.strictEqual(sms.tpdu, '0000');
|
|
});
|
|
|
|
test('service tables: UST service bits and IST fallback for 6F07', () => {
|
|
const ust = efDecServiceTable(efBytes('00000010'), EF_UST_SERVICES);
|
|
assert.deepStrictEqual(ust.services, [{ n: 29, name: 'Data download via SMS-CB' }]);
|
|
const ist = efDecImsi(efBytes('01'));
|
|
assert.deepStrictEqual(ist.services, [{ n: 1, name: 'P-CSCF address' }]);
|
|
});
|
|
|
|
test('decoder registry resolves by name first, then FID; unknown returns null', () => {
|
|
assert.strictEqual(efFindDecoder('EF.IMSI', null).fid, '6f07');
|
|
assert.strictEqual(efFindDecoder(null, '2fe2').name, 'EF.ICCID');
|
|
assert.strictEqual(efFindDecoder('ADF.USIM/EF.LOCI', null).name, 'EF.LOCI');
|
|
assert.strictEqual(efFindDecoder('EF.SUME', null).fid, '6f54');
|
|
assert.strictEqual(efFindDecoder('EF.NOPE', 'ABCD'), null);
|
|
// mixed-case registry names resolve case-insensitively without a FID
|
|
assert.strictEqual(efFindDecoder('EF.HPLMNwAcT', null).fid, '6f62');
|
|
assert.strictEqual(efFindDecoder('ef.plmnwact', null).name, 'EF.PLMNwAcT');
|
|
assert.strictEqual(efFidFromPath('ADF.USIM/6F07'), '6F07');
|
|
assert.strictEqual(efFidFromPath('MF/7F10/6F3A'), '6F3A');
|
|
assert.strictEqual(efFidFromPath('ADF.USIM'), null);
|
|
});
|
|
|
|
test('efDecodeFile handles transparent and record content', () => {
|
|
const t = efDecodeFile('EF.IMSI', null, { kind: 'transparent', data: '082982608200002080' });
|
|
assert.strictEqual(typeof t.fn, 'function');
|
|
assert.strictEqual(t.data.imsi, '228062800000208');
|
|
const r = efDecodeFile('EF.ADN', '6f3a', { kind: 'record', records: [
|
|
{ num: 1, data: '42204841203120536963FFFFFFFFFFFF06810628560810FFFFFFFFFFFFFF' },
|
|
{ num: 2, data: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' },
|
|
] });
|
|
assert.strictEqual(r.records.length, 2);
|
|
assert.strictEqual(r.records[0].data.name, 'B HA 1 Sic');
|
|
assert.strictEqual(r.records[0].empty, false);
|
|
assert.strictEqual(r.records[1].empty, true);
|
|
assert.strictEqual(efDecodeFile('EF.NOPE', null, { kind: 'transparent', data: '00' }), null);
|
|
});
|
|
|
|
test('efFlatten renders fields, services and PLMNs for display', () => {
|
|
assert.deepStrictEqual(efFlatten({ imsi: '123' }), [['IMSI', '123']]);
|
|
assert.deepStrictEqual(efFlatten({ services: [{ n: 5, name: 'Call Control by USIM' }] }),
|
|
[['Service 5', 'Call Control by USIM']]);
|
|
assert.deepStrictEqual(efFlatten({ plmns: [{ mcc: '262', mnc: '01', access_tech: 'GSM' }] }),
|
|
[['PLMN 1', '262-01 (GSM)']]);
|
|
assert.deepStrictEqual(efFlatten({ show_in_hplmn: true }), [['Show in HPLMN', 'yes']]);
|
|
assert.deepStrictEqual(efFlatten({ title: 'Menu!', icon: '0123' }), [['Title', 'Menu!'], ['Icon', '0123']]);
|
|
});
|
|
|
|
test('efDiffData reports only differing fields; efContentDiff decodes both sides', () => {
|
|
assert.deepStrictEqual(efDiffData({ imsi: '1', kc: 'AA' }, { imsi: '2', kc: 'AA' }), [['IMSI', '1', '2']]);
|
|
assert.deepStrictEqual(efDiffData({ imsi: '1' }, { imsi: '1' }), []);
|
|
const dd = efContentDiff('EF.IMSI', null, '082982608200002080', '082982608200002081');
|
|
assert.ok(dd && dd.rows.length === 1);
|
|
assert.strictEqual(dd.rows[0][0], 'IMSI');
|
|
assert.strictEqual(dd.rows[0][1], '228062800000208');
|
|
const same = efContentDiff('EF.IMSI', null, '082982608200002080', '092982608200002080');
|
|
assert.ok(same && same.sameDecoded);
|
|
assert.strictEqual(efContentDiff('EF.NOPE', null, '00', '01'), null);
|
|
});
|
|
|
|
test('efFlatten skips the all-FF empty marker', () => {
|
|
assert.deepStrictEqual(efFlatten({ empty: true }), []);
|
|
});
|
|
|
|
test('efDiffData names an all-FF side Empty (all FF)', () => {
|
|
assert.deepStrictEqual(efDiffData({ empty: true }, { full: 'Miran' }),
|
|
[['Full name', 'Empty (all FF)', 'Miran']]);
|
|
assert.deepStrictEqual(efDiffData({ full: 'Miran' }, { empty: true }),
|
|
[['Full name', 'Miran', 'Empty (all FF)']]);
|
|
assert.deepStrictEqual(efDiffData({ empty: true }, { empty: true }), []);
|
|
// a field merely missing from a non-empty record stays blank
|
|
assert.deepStrictEqual(efDiffData({ full: 'Miran' }, { full: 'Miran', short: 'Mir' }),
|
|
[['Short name', '', 'Mir']]);
|
|
});
|
|
|
|
test('efContentDiff reports an empty PNN record side', () => {
|
|
const expected = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
|
|
const actual = '430887CDB43CEC2687FFFFFFFFFFFFFFFFFFFFFF';
|
|
assert.deepStrictEqual(efDecPnn(efBytes(actual)), { full: 'Miran' });
|
|
const dd = efContentDiff('EF.PNN', '6fc5', expected, actual);
|
|
assert.ok(dd, 'decoded diff expected');
|
|
assert.deepStrictEqual(dd.rows, [['Full name', 'Empty (all FF)', 'Miran']]);
|
|
});
|
|
|
|
test('efDataSummary produces one-line summaries', () => {
|
|
assert.strictEqual(efDataSummary(efDecImsi(efBytes('082982608200002080'))), 'IMSI 228062800000208');
|
|
assert.strictEqual(efDataSummary(efDecIccid(efBytes('988812010000400310f0'))), 'ICCID 8988211000000430010');
|
|
assert.strictEqual(efDataSummary(efDecSume(efBytes('05054D656E7521FFFFFFFF'))), 'Menu!');
|
|
assert.strictEqual(efDataSummary({ plmns: [{ mcc: '262', mnc: '01' }] }), '262-01');
|
|
});
|
|
|
|
test('file manager decoded view renders fields, not JSON', () => {
|
|
const out = pysimFsDecodedHtml({ name: 'EF.IMSI', fid: null }, { success: true, data: '082982608200002080' });
|
|
assert.match(out, /IMSI/);
|
|
assert.match(out, /228062800000208/);
|
|
assert.match(out, /pySim JSON \(server\)/);
|
|
assert.doesNotMatch(out, /\{"imsi"/);
|
|
// the label column is content-sized, not a fixed 10rem block
|
|
assert.match(out, /grid-template-columns:max-content 1fr/);
|
|
assert.doesNotMatch(out, /w-40/);
|
|
// no decoder -> raw fallback with a note
|
|
const raw = pysimFsDecodedHtml({ name: 'EF.NOPE', fid: 'abcd' }, { success: true, data: '0011' });
|
|
assert.match(raw, /No decoder for this file/);
|
|
assert.match(raw, /0011/);
|
|
});
|
|
|
|
test('efRenderFieldsHtml sizes the label column to its content', () => {
|
|
const out = efRenderFieldsHtml([['ICCID', '8988'], ['Very long label', 'x']]);
|
|
assert.match(out, /style="display:grid;grid-template-columns:max-content 1fr/);
|
|
assert.doesNotMatch(out, /w-40/);
|
|
assert.ok(out.includes('>ICCID</span>'));
|
|
assert.ok(out.includes('>8988</span>'));
|
|
assert.ok(out.includes('>Very long label</span>'));
|
|
assert.match(efRenderFieldsHtml([]), /No decodable fields/);
|
|
});
|
|
|
|
test('profilerRenderReport shows raw and decoded comparisons for content mismatches', () => {
|
|
const res = {
|
|
path: 'ADF.USIM/6F07', name: 'EF.IMSI', status: 'fail',
|
|
checks: [{ label: 'content', expected: '082982608200002080', actual: '082982608200002081', ok: false }],
|
|
};
|
|
const out = profilerRenderReport([res], null);
|
|
// the raw pair is always shown
|
|
assert.match(out, /Raw comparison/);
|
|
assert.ok(out.includes('value="082982608200002080"'));
|
|
assert.ok(out.includes('value="082982608200002081"'));
|
|
// the decoded per-field diff follows, with expected/actual column headers
|
|
assert.match(out, /Decoded comparison/);
|
|
assert.match(out, /IMSI/);
|
|
assert.match(out, /228062800000208/);
|
|
assert.match(out, /228062800000218/);
|
|
assert.match(out, /<span class="flex-1 min-w-0 text-xs text-gray-500 dark:text-slate-400">expected<\/span><span class="flex-1 min-w-0 text-xs text-gray-500 dark:text-slate-400">actual<\/span>/);
|
|
// custom comparison labels are used for the raw rows and the decoded headers
|
|
const named = profilerRenderReport([res], { expected: 'master', actual: 'checked' });
|
|
assert.ok(named.includes('>master</span>'));
|
|
assert.ok(named.includes('>checked</span>'));
|
|
// matching decoded values -> note instead of the decoded table, raw pair kept
|
|
const same = profilerRenderReport([{
|
|
path: 'ADF.USIM/6F07', name: 'EF.IMSI', status: 'fail',
|
|
checks: [{ label: 'content', expected: '082982608200002080', actual: '092982608200002080', ok: false }],
|
|
}], null);
|
|
assert.match(same, /Raw comparison/);
|
|
assert.match(same, /Decoded values match/);
|
|
assert.doesNotMatch(same, /Decoded comparison/);
|
|
});
|