profiler: show raw-data mismatches as aligned monospace fields

FCI and content (incl. record) mismatches are now rendered as two read-only
monospace inputs — expected on top, actual directly beneath — sharing a
fixed-width right-aligned label column (w-24) so both fields start at
exactly the same horizontal position and stretch to fill the row
(flex-1/min-w-0, no overlap). A title attribute shows the full value on
hover. Non-raw checks (fileSize, recordLen, numRecords, content.records,
read failure) keep the inline line.

New profilerRawDataCheck helper; 2 new tests. SW cache v59 -> v60.
This commit is contained in:
2026-09-10 23:14:50 +03:00
parent ad543fd731
commit dbf9e8559b
3 changed files with 56 additions and 3 deletions
+21 -1
View File
@@ -7737,6 +7737,14 @@ function profilerNumRanges(nums) {
return parts.join(', ');
}
// Checks whose expected/actual are raw data (hex), rendered as aligned fields.
function profilerRawDataCheck(c) {
if (c.label === 'fci') return true;
if (/^content\.rec\d+$/.test(c.label)) return true;
if (c.label === 'content') return c.expected !== 'readable';
return false;
}
function profilerRenderReport(results) {
let html = '';
for (const r of results) {
@@ -7755,7 +7763,19 @@ function profilerRenderReport(results) {
if (r.error) html += '<div class="text-xs text-yellow-600 mt-1">' + esc(r.error) + '</div>';
for (const c of r.checks) {
if (c.ok) continue;
html += '<div class="text-xs text-red-600 mt-1">' + esc(c.label) + ': ' + esc(t('expected')) + ' <b>' + esc(String(c.expected)) + '</b>, ' + esc(t('actual')) + ' <b>' + esc(String(c.actual)) + '</b></div>';
if (profilerRawDataCheck(c)) {
html += '<div class="mt-1">';
html += '<div class="text-xs text-red-600 font-mono pl-2">' + esc(c.label) + '</div>';
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
'<span class="text-xs text-gray-500 dark:text-slate-400 w-24 text-right shrink-0">' + esc(t('expected')) + '</span>' +
'<input readonly title="' + escHtml(String(c.expected)) + '" value="' + escHtml(String(c.expected)) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
html += '<div class="flex items-center gap-2 mt-0.5 pl-2">' +
'<span class="text-xs text-gray-500 dark:text-slate-400 w-24 text-right shrink-0">' + esc(t('actual')) + '</span>' +
'<input readonly title="' + escHtml(String(c.actual)) + '" value="' + escHtml(String(c.actual)) + '" class="flex-1 min-w-0 font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 bg-gray-100 dark:bg-slate-800"></div>';
html += '</div>';
} else {
html += '<div class="text-xs text-red-600 mt-1">' + esc(c.label) + ': ' + esc(t('expected')) + ' <b>' + esc(String(c.expected)) + '</b>, ' + esc(t('actual')) + ' <b>' + esc(String(c.actual)) + '</b></div>';
}
}
const recFailed = (r.checks || []).some(c => /^content\.rec\d+$/.test(c.label) && !c.ok);
if (recFailed && r.recordsMatched && r.recordsMatched.length) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v59';
const CACHE = 'otaman-v60';
const URLS = [
'index.html',
'help.html',
+34 -1
View File
@@ -21,7 +21,7 @@ function extractFunc(src, name, asyncFn) {
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
}
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'profilerRenderReport'];
const FNS = ['profilerNormHex', 'profilerNormHexStrict', 'profilerMatch', 'profilerMatchMin', 'profilerMaskPrefix4', 'profilerFileFields', 'profilerContentKindForFileType', 'profilerEmptyRecordContent', 'profilerValidateProfile', 'profilerCustomNameForPath', 'profilerUpdateRulePath', 'profilerResultAspects', 'profilerAspectSummary', 'profilerNumRanges', 'esc', 'escHtml', 'profilerRawDataCheck', 'profilerRenderReport'];
let code = '';
for (const f of FNS) code += extractFunc(html, f) + '\n';
code += extractFunc(html, 'profilerBuildFileRule', true) + '\n';
@@ -535,3 +535,36 @@ test('profilerRenderReport includes the checked-aspects summary and matching-rec
assert.ok(html.includes('1-2, 5'));
delete global.t;
});
test('profilerRawDataCheck identifies raw-data checks', () => {
assert.strictEqual(profilerRawDataCheck({ label: 'fci' }), true);
assert.strictEqual(profilerRawDataCheck({ label: 'content', expected: 'AABBCC' }), true);
assert.strictEqual(profilerRawDataCheck({ label: 'content.rec3' }), true);
assert.strictEqual(profilerRawDataCheck({ label: 'content', expected: 'readable' }), false);
assert.strictEqual(profilerRawDataCheck({ label: 'content.records' }), false);
assert.strictEqual(profilerRawDataCheck({ label: 'fileSize' }), false);
});
test('profilerRenderReport renders raw-data mismatches as aligned readonly fields', () => {
global.t = s => s;
global.pysimCustomFiles = [];
const html = profilerRenderReport([
{ path: 'MF/6F07', name: 'EF.IMSI', status: 'fail', checks: [
{ label: 'fci', ok: false, expected: '621082024021', actual: '621082024022' },
] },
{ path: 'MF/7F20/6F3A', name: 'EF.X', status: 'fail', checks: [
{ label: 'content.records', ok: false, expected: '30 records', actual: '10 records' },
] },
]);
const fciBlock = html.split('</div>').find(s => s.includes('621082024021'));
assert.ok(html.includes('readonly'));
assert.ok((html.match(/readonly/g) || []).length >= 2);
assert.ok(html.includes('font-mono'));
assert.ok(html.includes('value="621082024021"'));
assert.ok(html.includes('value="621082024022"'));
assert.ok(html.includes('w-24 text-right'));
// non-raw check stays inline
assert.ok(html.includes('content.records: expected'));
assert.ok(!fciBlock.includes(': expected'));
delete global.t;
});