diff --git a/frontend/help-ru.html b/frontend/help-ru.html index d840ca5..b6f5d81 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -466,7 +466,7 @@ diff --git a/frontend/help.html b/frontend/help.html index fae53d9..728eabe 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -465,7 +465,7 @@ diff --git a/frontend/index.html b/frontend/index.html index 4d2326c..9d712dc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -19812,19 +19812,27 @@ function profilerRenderRule(i, r) { // built the same way; every change re-encodes the canonical decoded line the // check compares. Forms outside the supported subset stay raw (read-only, // removable). Capture fills the lines when a profile is created from a card -// or a snapshot; the radio toggles the check. +// or a snapshot; the radio toggles the check, None hides the whole block and +// the block collapses to its header (the rules are kept either way). function profilerRenderAccessRulesEditor(i, r) { const ar = r.accessRules || null; const lines = (ar && ar.expected) || []; const on = !!(ar && ar.enabled !== false); + const open = on && !!_profilerArrOpen[i]; let html = '
'; - html += '' + esc(t('Check access rules')) + ''; + if (on) { + html += ''; + } else { + html += '' + esc(t('Check access rules')) + ''; + } html += '
'; html += ''; html += ''; html += '
'; + if (!open) return html; if (r.fciMode === 'exact') { - html += ''; + html += ''; } for (let ri = 0; ri < lines.length; ri++) { html += profilerArrRuleHtml(i, ri, lines[ri]); @@ -19836,6 +19844,15 @@ 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(); +} + // 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) { @@ -19972,6 +19989,8 @@ function profilerSetAccessRules(i, on) { const r = profilerDraft.rules[i]; if (!r.accessRules) r.accessRules = { expected: [] }; r.accessRules.enabled = on; + // Check opens the block, None hides it (the rules are kept either way) + _profilerArrOpen[i] = on; profilerRenderEditor(); } diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js index 02999bd..a450cdf 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', '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', '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'; @@ -37,6 +37,7 @@ code += extractFunc(html, 'profilerScanSnapshot', true) + '\n'; code += extractFunc(html, 'pysimArrEnsure', true) + '\n'; code += "var _scanTarget = 'profile';\nvar profilerResults = null;\nvar profilerResultsHeader = null;\nvar profilerMismatchOnly = false;\n"; code += "var _pysimArrCache = null;\nvar _pysimCardSession = null;\n"; +code += "var _profilerArrOpen = {};\n"; // the view-history hooks (the router lives in the page; this pure test stubs it) code += 'globalThis.navRecord = function() {};\nglobalThis.navReplace = function() {};\n'; code += html.match(/const PROFILER_MASK_PREFIX4_FIDS = \{[\s\S]*?\n\};/)[0] + '\n'; @@ -1114,23 +1115,45 @@ test('the access-rules editor renders structured rows', () => { 'RESIZE FILE (AM_DO 0x84, INS 0xD4): never', 'AM 0x02 (UPDATE/ERASE (EF) / CREATE FILE EF (DF)): (PIN app1 (verify) OR PIN app2 (verify))', ] } }; - const html = profilerRenderAccessRulesEditor(3, r); + // collapsed by default: the header + toggle carry the rule count, no rows + _profilerArrOpen = {}; + const closed = profilerRenderAccessRulesEditor(3, r); + assert.ok(closed.includes('profilerArrToggleOpen(3)'), closed); + assert.ok(closed.includes('Check access rules (3)'), closed); + assert.ok(!closed.includes('profilerArrToggleBit(3,0,1'), closed); + assert.ok(!closed.includes('profilerArrAddRule(3)'), closed); + // expanded: structured rows + _profilerArrOpen = { 3: true }; + const out = profilerRenderAccessRulesEditor(3, r); // the READ bit is checked; the condition select shows always - assert.ok(html.includes('type="checkbox" checked onchange="profilerArrToggleBit(3,0,1,this.checked)"'), html); - assert.ok(html.includes(''), html); - assert.ok(html.includes(''), html); + assert.ok(out.includes('type="checkbox" checked onchange="profilerArrToggleBit(3,0,1,this.checked)"'), out); + assert.ok(out.includes(''), out); + assert.ok(out.includes(''), out); // the RESIZE row uses the command form, not bits - assert.ok(html.includes(''), html); + assert.ok(out.includes(''), out); // the template rule is raw and not editable - assert.ok(html.includes('raw: AM 0x02'), html); - assert.ok(!html.includes('profilerArrToggleBit(3,2'), html); + assert.ok(out.includes('raw: AM 0x02'), out); + assert.ok(!out.includes('profilerArrToggleBit(3,2'), out); // the preview lines and the + rule button are present - assert.ok(html.includes('RESIZE FILE (AM_DO 0x84, INS 0xD4): never'), html); - assert.ok(html.includes('profilerArrAddRule(3)'), html); - // an empty rule shows the capture hint with no disabled controls - const empty = profilerRenderAccessRulesEditor(4, { fciMode: 'type_size', accessRules: null }); + assert.ok(out.includes('RESIZE FILE (AM_DO 0x84, INS 0xD4): never'), out); + assert.ok(out.includes('profilerArrAddRule(3)'), out); + // None hides the whole block (no toggle, no body) + const none = profilerRenderAccessRulesEditor(3, { ...r, accessRules: { ...r.accessRules, enabled: false } }); + assert.ok(none.includes('Check access rules'), none); + assert.ok(!none.includes('profilerArrToggleOpen'), none); + assert.ok(!none.includes('profilerArrAddRule'), none); + // a rule without accessRules starts hidden too (Check enables it) + const absent = profilerRenderAccessRulesEditor(4, { fciMode: 'type_size', accessRules: null }); + assert.ok(!absent.includes('profilerArrToggleOpen'), absent); + assert.ok(!absent.includes('profilerArrAddRule'), absent); + // an enabled but empty block shows the capture hint and + rule + _profilerArrOpen = { 4: true }; + const empty = profilerRenderAccessRulesEditor(4, { fciMode: 'type_size', accessRules: { expected: [] } }); assert.ok(empty.includes('profilerArrAddRule(4)'), empty); assert.ok(!empty.includes(' disabled'), empty); + _profilerArrOpen = {}; + // Check opens the block, None hides it + assert.ok(html.includes('_profilerArrOpen[i] = on;'), 'the radio drives the visibility'); delete global.t; });