b0b8d02fe7
The parent field was a select over custom DFs only, so a standard DF (e.g. MF/7F20) could not be a parent and canonical entries under one were dropped on import. It is now a free-text path with suggestions from the root, custom DFs and DFs of the loaded tree (any depth); a bare FID chain is completed from the root selector. Validation accepts standard or not-yet-seen parents (flagged in the list), rejects a parent known to be an EF, and import keeps such entries. Custom-file edits re-inject the loaded tree: new nodes appear at once, renamed model nodes are restored and injected nodes removed when their entry goes away. Help updated (the form lives in Profiler, not Card reader). SW cache otaman-v176.
300 lines
13 KiB
JavaScript
300 lines
13 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);
|
|
}
|
|
|
|
let code = 'var pysimCustomFiles = [];\nvar pysimCustomEditIndex = null;\nvar _pysimCustomDropped = 0;\nvar pysimFsTreeRoot = null;\n';
|
|
for (const fn of ['pysimCustomNormPath', 'pysimCustomKindForName', 'pysimCustomFid',
|
|
'pysimCustomParent', 'pysimCustomRoot', 'pysimCustomKnownDfPaths', 'pysimCustomParentStatus',
|
|
'pysimCustomValidate', 'pysimCustomRewriteDescendants', 'pysimCustomNormalizeEntries',
|
|
'pysimCustomSave', 'pysimCustomRenderRoots', 'pysimCustomRenderParents',
|
|
'pysimCustomRootChanged', 'pysimCustomParentInput', 'pysimCustomSubmit',
|
|
'pysimCustomEdit', 'pysimCustomEditCancel', 'pysimCustomRemove', 'pysimCustomRender',
|
|
'pysimFsNodePath', 'pysimFsFindNodeByPath', 'pysimCustomInject', 'pysimCustomRefreshTree']) {
|
|
code += extractFunc(html, fn) + '\n';
|
|
}
|
|
code += html.match(/const CUSTOM_ROOTS = \[[^\]]*\];/)[0].replace('const ', 'var ') + '\n';
|
|
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\nglobalThis.pysimFsRenderTree = () => {};\n';
|
|
eval(code);
|
|
|
|
function fakeEl(id) {
|
|
const classes = new Set();
|
|
return {
|
|
id, value: '', innerHTML: '', textContent: '', attrs: {}, focused: 0,
|
|
classList: {
|
|
add: (...cs) => cs.forEach(c => classes.add(c)),
|
|
remove: (...cs) => cs.forEach(c => classes.delete(c)),
|
|
contains: c => classes.has(c),
|
|
},
|
|
setAttribute(k, v) { this.attrs[k] = v; },
|
|
focus() { this.focused++; },
|
|
options() { return [...this.innerHTML.matchAll(/value="([^"]+)"/g)].map(m => m[1]); },
|
|
};
|
|
}
|
|
|
|
function setup(entries) {
|
|
const els = {
|
|
'pysim-cf-root': fakeEl('pysim-cf-root'),
|
|
'pysim-cf-parent': fakeEl('pysim-cf-parent'),
|
|
'pysim-cf-parent-list': fakeEl('pysim-cf-parent-list'),
|
|
'pysim-cf-fid': fakeEl('pysim-cf-fid'),
|
|
'pysim-cf-name': fakeEl('pysim-cf-name'),
|
|
'pysim-cf-list': fakeEl('pysim-cf-list'),
|
|
'pysim-cf-add-btn': fakeEl('pysim-cf-add-btn'),
|
|
'pysim-cf-cancel-btn': fakeEl('pysim-cf-cancel-btn'),
|
|
};
|
|
els['pysim-cf-cancel-btn'].classList.add('hidden');
|
|
const store = {};
|
|
globalThis.localStorage = {
|
|
getItem: k => (k in store ? store[k] : null),
|
|
setItem: (k, v) => { store[k] = String(v); },
|
|
};
|
|
globalThis.document = { getElementById: id => els[id] || null };
|
|
globalThis.alertCalls = [];
|
|
globalThis.alert = m => { globalThis.alertCalls.push(m); };
|
|
globalThis.confirmResult = true;
|
|
globalThis.confirm = () => globalThis.confirmResult;
|
|
pysimCustomFiles = (entries || []).map(e => Object.assign({}, e));
|
|
pysimCustomEditIndex = null;
|
|
_pysimCustomDropped = 0;
|
|
pysimFsTreeRoot = null;
|
|
pysimCustomRenderRoots();
|
|
pysimCustomRenderParents();
|
|
return els;
|
|
}
|
|
|
|
function fill(els, root, parent, fid, name) {
|
|
els['pysim-cf-root'].value = root;
|
|
pysimCustomRenderParents();
|
|
els['pysim-cf-parent'].value = parent;
|
|
els['pysim-cf-fid'].value = fid;
|
|
els['pysim-cf-name'].value = name;
|
|
}
|
|
|
|
test('helpers derive canonical roots, parents, FIDs and kinds', () => {
|
|
assert.strictEqual(pysimCustomNormPath('3f00/7f20/6f46'), 'MF/7F20/6F46');
|
|
assert.strictEqual(pysimCustomNormPath(' mf / a153 '), 'MF/A153');
|
|
assert.strictEqual(pysimCustomNormPath(''), '');
|
|
assert.strictEqual(pysimCustomRoot({ path: 'ADF.USIM/6F07' }), 'ADF.USIM');
|
|
assert.strictEqual(pysimCustomParent({ path: 'MF/A153/4954' }), 'MF/A153');
|
|
assert.strictEqual(pysimCustomParent({ path: 'MF/6F46' }), 'MF');
|
|
assert.strictEqual(pysimCustomFid({ path: 'MF/A153/4954' }), '4954');
|
|
assert.strictEqual(pysimCustomKindForName('EF.SPN'), 'ef');
|
|
assert.strictEqual(pysimCustomKindForName('DF.GSM'), 'df');
|
|
assert.strictEqual(pysimCustomKindForName('XX.SPN'), null);
|
|
});
|
|
|
|
test('migration resolves legacy relative paths and drops the unresolvable', () => {
|
|
const res = pysimCustomNormalizeEntries([
|
|
{ path: '3f00/a153', name: 'DF.A1' },
|
|
{ path: 'a153/4954', name: 'EF.SPNS', fid: '4954', parentFid: 'a153' },
|
|
{ path: 'a153/4955', name: 'EF.SMSCS' },
|
|
{ path: 'ffff/1111', name: 'EF.ORPHAN' }, // no such custom DF -> dropped
|
|
{ path: 'MF/6F46', name: 'BAD.NAME' }, // invalid alias -> dropped
|
|
], []);
|
|
assert.deepStrictEqual(res.files, [
|
|
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
|
|
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
|
|
{ path: 'MF/A153/4955', name: 'EF.SMSCS', kind: 'ef' },
|
|
]);
|
|
assert.strictEqual(res.dropped, 2);
|
|
});
|
|
|
|
test('validation accepts standard/unknown parents but rejects known EFs', () => {
|
|
const files = [{ path: 'MF/A153', name: 'DF.A1', kind: 'df' }];
|
|
// a parent that is not a custom entry is accepted: it may be a standard DF
|
|
// from the card model, or simply not seen in the tree yet
|
|
const unknown = pysimCustomValidate('MF', 'MF/A999', '6F46', 'EF.SPN', files, null);
|
|
assert.strictEqual(unknown.error, null);
|
|
assert.strictEqual(unknown.path, 'MF/A999/6F46');
|
|
// parent defined -> ok
|
|
assert.strictEqual(pysimCustomValidate('MF', 'MF/A153', '6F46', 'EF.SPN', files, null).path, 'MF/A153/6F46');
|
|
// a bare FID chain typed without the root is completed from the selector
|
|
assert.strictEqual(pysimCustomValidate('MF', 'A153', '6F46', 'EF.SPN', files, null).path, 'MF/A153/6F46');
|
|
// deep chains are fine
|
|
assert.strictEqual(pysimCustomValidate('MF', 'MF/7F20/5F01', '6F46', 'EF.DEEP', [], null).path, 'MF/7F20/5F01/6F46');
|
|
// a parent known here to be an EF is rejected
|
|
assert.match(pysimCustomValidate('MF', 'MF/A153/6F46', '1234', 'EF.X',
|
|
files.concat([{ path: 'MF/A153/6F46', name: 'EF.OTHER', kind: 'ef' }]), null).error, /not a DF/);
|
|
// parent segments must be 4-hex FIDs
|
|
assert.match(pysimCustomValidate('MF', 'MF/FOO', '6F46', 'EF.SPN', files, null).error, /4-hex/);
|
|
// bad FID
|
|
assert.match(pysimCustomValidate('MF', 'MF', '6F4', 'EF.SPN', files, null).error, /4 hex/);
|
|
// bad alias
|
|
assert.match(pysimCustomValidate('MF', 'MF', '6F46', 'SPN', files, null).error, /EF\.|DF\./);
|
|
// duplicate
|
|
assert.match(pysimCustomValidate('MF', 'MF/A153', '6F46', 'EF.SPN',
|
|
files.concat([{ path: 'MF/A153/6F46', name: 'EF.OTHER', kind: 'ef' }]), null).error, /already defined/);
|
|
// editing the same entry is not a duplicate
|
|
assert.strictEqual(pysimCustomValidate('MF', 'MF/A153', '6F46', 'EF.SPN',
|
|
files.concat([{ path: 'MF/A153/6F46', name: 'EF.OTHER', kind: 'ef' }]), 1).error, null);
|
|
// root must be known
|
|
assert.match(pysimCustomValidate('MFX', 'MFX', '6F46', 'EF.SPN', files, null).error, /Root/);
|
|
});
|
|
|
|
test('root and parent suggestions list roots, custom DFs and tree DFs', () => {
|
|
const els = setup([
|
|
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
|
|
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
|
|
{ path: 'ADF.USIM/6F07', name: 'EF.IMSI', kind: 'ef' },
|
|
]);
|
|
assert.deepStrictEqual(els['pysim-cf-root'].options(), ['MF', 'ADF.USIM', 'ADF.ISIM']);
|
|
let parents = els['pysim-cf-parent-list'].options();
|
|
assert.ok(parents.includes('MF'));
|
|
assert.ok(parents.includes('MF/A153'));
|
|
assert.ok(!parents.includes('MF/A153/4954'), 'EFs are not parent options');
|
|
// DFs known from the loaded file tree are suggested, at any depth
|
|
pysimFsTreeRoot = { name: 'MF', fid: '3F00', isDir: true, parent: null, children: [] };
|
|
const df = { name: 'DF.TELECOM', fid: '7F10', isDir: true, parent: pysimFsTreeRoot, children: [] };
|
|
const sub = { name: 'DF.SUB', fid: '5F01', isDir: true, parent: df, children: [] };
|
|
df.children = [sub];
|
|
pysimFsTreeRoot.children = [df];
|
|
pysimCustomRenderParents();
|
|
parents = els['pysim-cf-parent-list'].options();
|
|
assert.ok(parents.includes('MF/7F10'));
|
|
assert.ok(parents.includes('MF/7F10/5F01'));
|
|
// the ADF root is always a valid parent
|
|
els['pysim-cf-root'].value = 'ADF.USIM';
|
|
pysimCustomRootChanged();
|
|
assert.deepStrictEqual(els['pysim-cf-parent-list'].options(), ['ADF.USIM']);
|
|
});
|
|
|
|
test('parent status classifies root, custom, tree and unknown parents', () => {
|
|
setup([
|
|
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
|
|
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
|
|
]);
|
|
assert.strictEqual(pysimCustomParentStatus('MF').status, 'root');
|
|
assert.strictEqual(pysimCustomParentStatus('MF/A153').status, 'df');
|
|
assert.strictEqual(pysimCustomParentStatus('MF/A153/4954').status, 'not-df');
|
|
assert.strictEqual(pysimCustomParentStatus('MF/FFFF').status, 'unknown');
|
|
pysimFsTreeRoot = { name: 'MF', fid: '3F00', isDir: true, parent: null, children: [] };
|
|
const df = { name: 'DF.GSM', fid: '7F20', isDir: true, parent: pysimFsTreeRoot, children: [] };
|
|
df.children = [{ name: 'EF.SPN', fid: '6F46', isDir: false, parent: df, children: null }];
|
|
pysimFsTreeRoot.children = [df];
|
|
assert.strictEqual(pysimCustomParentStatus('MF/7F20').status, 'df');
|
|
assert.strictEqual(pysimCustomParentStatus('MF/7F20/6F46').status, 'not-df');
|
|
assert.strictEqual(pysimCustomParentStatus('MF/7F20/9999').status, 'unknown');
|
|
});
|
|
|
|
test('import keeps canonical entries whose parent is outside the custom list', () => {
|
|
const res = pysimCustomNormalizeEntries([
|
|
{ path: '3F00/7f20/5f01/6f46', name: 'EF.DEEP' },
|
|
{ path: 'mf/ffff/6f46', name: 'EF.ORPHAN' },
|
|
{ path: 'MF/6F46', name: 'NOPE' },
|
|
], []);
|
|
assert.deepStrictEqual(res.files, [
|
|
{ path: 'MF/7F20/5F01/6F46', name: 'EF.DEEP', kind: 'ef' },
|
|
{ path: 'MF/FFFF/6F46', name: 'EF.ORPHAN', kind: 'ef' },
|
|
]);
|
|
assert.strictEqual(res.dropped, 1);
|
|
});
|
|
|
|
test('submit adds files under the root and under a defined DF', () => {
|
|
const els = setup([]);
|
|
fill(els, 'MF', 'MF', '6F46', 'EF.SPN');
|
|
pysimCustomSubmit();
|
|
assert.deepStrictEqual(pysimCustomFiles, [{ path: 'MF/6F46', name: 'EF.SPN', kind: 'ef' }]);
|
|
assert.strictEqual(els['pysim-cf-fid'].value, '');
|
|
// add a DF, then a file under it
|
|
fill(els, 'MF', 'MF', 'A153', 'DF.A1');
|
|
pysimCustomSubmit();
|
|
fill(els, 'MF', 'MF/A153', '4954', 'EF.SPNS');
|
|
pysimCustomSubmit();
|
|
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path),
|
|
['MF/6F46', 'MF/A153', 'MF/A153/4954']);
|
|
// a parent that is not a custom entry is allowed (it may be a standard DF
|
|
// the tree has not loaded yet)
|
|
fill(els, 'MF', 'MF/A153', '2222', 'EF.ORPHAN');
|
|
pysimCustomSubmit();
|
|
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path),
|
|
['MF/6F46', 'MF/A153', 'MF/A153/4954', 'MF/A153/2222']);
|
|
assert.strictEqual(globalThis.alertCalls.length, 0);
|
|
// a parent known here to be an EF is rejected
|
|
fill(els, 'MF', 'MF/A153/4954', '3333', 'EF.NOPE');
|
|
pysimCustomSubmit();
|
|
assert.deepStrictEqual(globalThis.alertCalls, ['Parent is not a DF: MF/A153/4954']);
|
|
});
|
|
|
|
test('editing a DF FID rewrites its descendants', () => {
|
|
const els = setup([
|
|
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
|
|
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
|
|
{ path: 'MF/A153/4955', name: 'EF.SMSCS', kind: 'ef' },
|
|
]);
|
|
pysimCustomEdit(0);
|
|
assert.strictEqual(els['pysim-cf-fid'].value, 'A153');
|
|
els['pysim-cf-fid'].value = 'A154';
|
|
pysimCustomSubmit();
|
|
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path),
|
|
['MF/A154', 'MF/A154/4954', 'MF/A154/4955']);
|
|
assert.strictEqual(pysimCustomEditIndex, null);
|
|
});
|
|
|
|
test('editing an entry to another defined path is rejected', () => {
|
|
const els = setup([
|
|
{ path: 'MF/6F46', name: 'EF.SPN', kind: 'ef' },
|
|
{ path: 'MF/6F44', name: 'EF.SPN2', kind: 'ef' },
|
|
]);
|
|
pysimCustomEdit(0);
|
|
els['pysim-cf-fid'].value = '6F44';
|
|
pysimCustomSubmit();
|
|
assert.deepStrictEqual(globalThis.alertCalls, ['File already defined: MF/6F44']);
|
|
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path), ['MF/6F46', 'MF/6F44']);
|
|
assert.strictEqual(pysimCustomEditIndex, 0);
|
|
});
|
|
|
|
test('deleting a DF cascades to its children only after confirmation', () => {
|
|
setup([
|
|
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
|
|
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
|
|
{ path: 'MF/6F46', name: 'EF.SPN', kind: 'ef' },
|
|
]);
|
|
globalThis.confirmResult = false;
|
|
pysimCustomRemove(0);
|
|
assert.strictEqual(pysimCustomFiles.length, 3, 'cancelled delete must keep everything');
|
|
globalThis.confirmResult = true;
|
|
pysimCustomRemove(0);
|
|
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path), ['MF/6F46']);
|
|
});
|
|
|
|
test('render shows the canonical path, kind and row actions', () => {
|
|
const els = setup([
|
|
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
|
|
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
|
|
]);
|
|
pysimCustomRender();
|
|
const out = els['pysim-cf-list'].innerHTML;
|
|
assert.match(out, /MF\/A153/);
|
|
assert.match(out, /MF\/A153\/4954/);
|
|
assert.match(out, /\(DF\)/);
|
|
assert.match(out, /pysimCustomEdit\(0\)/);
|
|
assert.match(out, /pysimCustomRemove\(1\)/);
|
|
});
|
|
|
|
test('render flags dropped legacy entries', () => {
|
|
const els = setup([]);
|
|
_pysimCustomDropped = 3;
|
|
pysimCustomRender();
|
|
assert.match(els['pysim-cf-list'].innerHTML, /3/);
|
|
_pysimCustomDropped = 0;
|
|
});
|