diff --git a/frontend/index.html b/frontend/index.html index 9d712dc..fc6d5ef 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -19545,6 +19545,8 @@ function profilerEdit(i) { const p = profiles[i]; profilerEditId = p.id; profilerDraft = JSON.parse(JSON.stringify(p)); + // a fresh draft starts with every access-rules block collapsed + _profilerArrOpen = {}; profilerSetView('editor'); document.getElementById('profiler-name').value = profilerDraft.name; profilerRenderEditor(); @@ -19733,6 +19735,7 @@ function profilerAddRule() { function profilerDeleteRule(i) { if (!profilerDraft) return; profilerDraft.rules.splice(i, 1); + _profilerArrOpen = profilerArrShiftOpen(_profilerArrOpen, i); profilerRenderEditor(); } @@ -19807,6 +19810,11 @@ function profilerRenderRule(i, r) { return html; } +// The expanded access-rule blocks of the profile editor (transient UI state, +// kept across the editor re-renders; None hides the block entirely; a fresh +// draft resets it and a deleted rule shifts the keys). +let _profilerArrOpen = {}; + // Access-rules check editor (v3.25.0): captured rules open as structured rows // (operation bits / command + security conditions) and hand-made rules can be // built the same way; every change re-encodes the canonical decoded line the @@ -19844,15 +19852,23 @@ function profilerRenderAccessRulesEditor(i, r) { return html; } -// The expanded access-rule blocks of the profile editor (transient UI state, -// kept across the editor re-renders; None hides the block entirely). -let _profilerArrOpen = {}; - function profilerArrToggleOpen(i) { _profilerArrOpen[i] = !_profilerArrOpen[i]; profilerRenderEditor(); } +// Re-key the open-state map after a rule at `removed` was deleted: the +// entries below stay, the ones above shift down by one. +function profilerArrShiftOpen(open, removed) { + const next = {}; + for (const k of Object.keys(open)) { + const n = Number(k); + if (n < removed) next[n] = open[k]; + else if (n > removed) next[n - 1] = open[k]; + } + return next; +} + // One expected rule row: the operation form + bit checkboxes (or the RESIZE / // INCREASE command), the condition rows and the canonical preview line. function profilerArrRuleHtml(i, ri, line) { diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index a450cdf..64b29cd 100644 --- a/frontend/tests/profiler.test.js +++ b/frontend/tests/profiler.test.js @@ -22,7 +22,7 @@ function extractFunc(src, name, asyncFn) { } const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'pysimCustomNormPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'profilerMatchedRecordsText', 'profilerCloneName', 'profilerClone', 'profilerNewId', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport', 'parseBerLen', 'parseTlvList', 'fcpInt', 'fcpParseTlvs', 'fcpFileDescriptor', 'fcpLifeCycle', 'fcpSfi', 'fcpDo', 'fcpDecode', - 'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath', + 'fcpArrRef', 'fcpSeidName', 'fcpArrRules', 'fcpArrRefFromFci', 'fcpWithoutArrRef', 'fcpArrSplitTop', 'fcpArrKeyRefFromName', 'fcpArrKeyRefOptions', 'fcpArrScParse', 'fcpArrScEncode', 'fcpArrRuleParse', 'fcpArrRuleEncode', 'fcpArrRuleToggleBit', 'fcpArrRuleSetKind', 'fcpArrRuleSetSc', 'fcpArrRuleSetUq', 'fcpArrRuleAddSc', 'fcpArrRuleRemoveSc', 'profilerRenderAccessRulesEditor', 'profilerArrRuleHtml', 'profilerArrScSelect', 'profilerArrUqSelect', 'profilerArrToggleOpen', 'profilerArrShiftOpen', 'profilerAccessRulesDiffHtml', 'profilerSnapshotArr', 'efBytes', 'efHex', 'efTlv', 'efDecArr', 'efArrAmLabel', 'efArrSc', 'efArrAmDo', 'efFidFromPath', 'pinKeyRefName', 'fcpPsTemplate', 'fcpDiffHtml', 'profilerFciPreviewItems', 'profilerUpdateFciPreview', 'profilerUpdateRule', 'profilerFciInput', 'profilerScanToggleAll', 'profilerScanIgnoreAllState', 'swapNibbles', 'decIccid', 'profilerSnapshotIccid', 'profilerValidateSnapshot', 'profilerListSwitch', 'profilerScanRefreshOptions', 'profilerLiveSource', 'profilerSnapshotSource', 'profilerVisibleResults', 'profilerRulesFromSnapshot', 'profilerExtraFileResults', 'profilerScanNameKeydown', 'profilerTimingStats', 'profilerTimingAccumulator', 'profilerFormatMs', 'profilerRenderSnapshotSummary', 'profilerSnapshotCountLabel', 'pysimFsInfoHtml', 'profilerLabelText', 'profilerResultsHeaderText', 'profilerRenderResultsView', 'profilerBuildFileRuleFromSnapshot', 'profilerSnapshotPickListHtml', 'profilerScanSetTarget']; let code = ''; for (const f of FNS) code += extractFunc(html, f) + '\n'; @@ -1078,7 +1078,7 @@ test('fcpArrRuleParse/Encode round-trips the decoded rule forms', () => { for (const raw of [ '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)): SM CRT 0xB4', - 'command description 0x84 (D4)', + 'command description 0x85 (D4)', ]) { const rm = fcpArrRuleParse(raw); assert.strictEqual(rm.raw, raw, raw); @@ -1104,8 +1104,8 @@ test('fcpArrRule mutators re-encode the canonical line', () => { m = fcpArrRuleSetSc(m, 0, 'never'); assert.strictEqual(fcpArrRuleEncode(m), 'RESIZE FILE (AM_DO 0x84, INS 0xD4): never'); // a raw model is never modified - const raw = fcpArrRuleParse('command description 0x84 (D4)'); - assert.strictEqual(fcpArrRuleEncode(fcpArrRuleToggleBit(raw, 0x01, true)), 'command description 0x84 (D4)'); + const raw = fcpArrRuleParse('command description 0x85 (D4)'); + assert.strictEqual(fcpArrRuleEncode(fcpArrRuleToggleBit(raw, 0x01, true)), 'command description 0x85 (D4)'); }); test('the access-rules editor renders structured rows', () => { @@ -1154,9 +1154,17 @@ test('the access-rules editor renders structured rows', () => { _profilerArrOpen = {}; // Check opens the block, None hides it assert.ok(html.includes('_profilerArrOpen[i] = on;'), 'the radio drives the visibility'); + // a fresh draft resets the open blocks + assert.ok(html.includes("_profilerArrOpen = {};\n\tprofilerSetView('editor');"), 'profilerEdit resets the blocks'); delete global.t; }); +test('the access-rule open state survives rule deletion', () => { + assert.deepStrictEqual(profilerArrShiftOpen({ 0: true, 2: true, 3: true }, 1), { 0: true, 1: true, 2: true }); + assert.deepStrictEqual(profilerArrShiftOpen({ 0: true, 2: true, 3: true }, 2), { 0: true, 2: true }); + assert.deepStrictEqual(profilerArrShiftOpen({}, 0), {}); +}); + test('profilerResultAspects and the summary include Access rules', () => { const res = { checks: [ { label: 'exists', ok: true },