fix: all tabs inside the page container + compact RAM row (v3.6.46)

The max-w-7xl mx-auto page container closed before #tab-scp81, so SCP81
and Cards rendered directly under <body> and stretched to the whole window
while the other five tabs were capped at 1280 px.  The closing </div> moved
after #tab-cards (before the modals); a structural test walks the markup
and asserts every tab-content has the container as an ancestor (verified to
fail on the old nesting with 'tab-scp81 must be inside the max-w-7xl page
container').

The SCP80/RAM form's Card preset and Operation selects now share one
flex gap-2 items-end row with Execute (guarded by a markup test).

sw cache simple-v319; tests: 531 Python / 683 Node.
This commit is contained in:
2026-09-29 23:38:47 +03:00
parent cf9c9b7bab
commit 55adeb0fbd
5 changed files with 49 additions and 12 deletions
+4 -5
View File
@@ -683,14 +683,13 @@
<div id="scp80-sub-ram" class="hidden">
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="RAM operations perform atomic GlobalPlatform commands over SCP80. Card keys are taken from the saved preset.">RAM operations perform atomic GlobalPlatform commands over SCP80. Card keys are taken from the saved preset.</p>
<div class="mb-3">
<div class="mb-3 flex gap-2 items-end">
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Card preset">Card preset</label>
<select id="ram-card-sel" onchange="ramApplyCard(this.value)" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="" data-l10n="— Select card —">— Select card —</option>
</select>
</div>
<div class="mb-3 flex gap-2 items-end">
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Operation">Operation</label>
<select id="ram-op" onchange="ramOpChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
@@ -1354,7 +1353,6 @@
</fieldset>
</div>
</div>
</div>
<div id="tab-scp81" class="tab-content hidden">
<div class="flex flex-wrap justify-center gap-2 mb-3">
@@ -1603,6 +1601,7 @@
</div>
<div id="cards-io" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400"></div>
</div>
</div>
<div id="event-send-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 max-w-sm w-full mx-4 max-h-[85vh] overflow-auto">
@@ -1747,7 +1746,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.6.45';
const SIMPLE_VERSION = '3.6.46';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v318';
const CACHE = 'simple-v319';
const URLS = [
'index.html',
'help.html',
+38
View File
@@ -448,3 +448,41 @@ test('every data-l10n attribute resolves in LANG_RU after HTML decoding', () =>
const missing = [...new Set([...attrs, ...titles].filter(k => !(k in dict)))];
assert.deepStrictEqual(missing, [], 'data-l10n keys with no LANG_RU entry:\n' + missing.join('\n'));
});
test('every tab-content lives inside the max-w-7xl page container', () => {
// SCP81 and Cards used to sit outside the container (its closing </div>
// came before them), so they stretched to the whole window while the
// other tabs were capped - v3.6.46.
const markup = htmlOnly(html);
const re = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*?)>/g;
let m;
let depth = 0;
let containerDepth = null;
const seen = new Set();
while ((m = re.exec(markup))) {
const closing = m[1] === '/';
const tag = m[2].toLowerCase();
const attrs = m[3] || '';
const selfClosing = attrs.trim().endsWith('/');
if (VOID_TAGS.has(tag) || selfClosing) continue;
if (closing) {
if (containerDepth !== null && depth === containerDepth) containerDepth = null;
depth--;
continue;
}
if (tag === 'div' && /class="[^"]*max-w-7xl/.test(attrs)) containerDepth = depth + 1;
if (tag === 'div' && /class="[^"]*tab-content/.test(attrs)) {
const id = (/id="([^"]+)"/.exec(attrs) || [])[1];
assert.notStrictEqual(containerDepth, null,
id + ' must be inside the max-w-7xl page container');
seen.add(id);
}
depth++;
}
assert.strictEqual(seen.size, 7, 'expected all seven tabs, saw ' + seen.size);
});
test('the RAM form shows Card preset and Operation in one row', () => {
const row = /<div class="mb-3 flex gap-2 items-end">\s*<div class="flex-1">\s*<label[^>]*data-l10n="Card preset"[\s\S]*?id="ram-card-sel"[\s\S]*?data-l10n="Operation"[\s\S]*?id="ram-op"[\s\S]*?data-l10n="Execute"/.exec(html);
assert.ok(row, 'Card preset + Operation + Execute must share one row');
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.6.45"
version = "3.6.46"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.45'
VERSION = '3.6.46'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE