v3.11.0 review fixes: ADM length, TAR re-render hygiene

- the ADM input capped the key at 8 chars, but the server's _verify_adm
  takes 4-16 hex digits (padded to 8 CHV bytes) and the old input had no
  limit; maxlength=16 now, with a static guard in html.test.js.
- cardsTarAdd/cardsTarRemove re-rendered the table from the save-path
  reader, so an emptied role field was silently refilled with its default.
  The renderer no longer invents defaults; a raw reader (cardsTarsFromTable)
  feeds Add/Remove and the language-switch refresh, while the save path
  (cardsTarsFromForm) keeps the default-filling for the mandatory roles.
- the editor's dynamic TAR/keyset rows now rebuild on a language switch
  (refreshDynamicI18n) from what is typed - their placeholders and titles
  carry t() strings.  (The title/Save label were already refreshed there.)
- dropped the now-unused cardsRoleMsl helper and its test references.
- docs: the ADM is documented as 4-16 hex digits (help EN/RU, api.md,
  AGENTS).  sw cache simple-v362.

Tests: 665 Python + 784 frontend green; the whole inline PWA script
syntax-checked.
This commit is contained in:
2026-10-02 10:27:42 +03:00
parent 65c565e058
commit 5a3c4c3529
9 changed files with 63 additions and 31 deletions
+1 -1
View File
@@ -243,7 +243,7 @@ pass `spi1` itself, otherwise it is refused with `400` and a hint.
**Card codes.** The optional `pin1`/`puk1`/`pin2`/`puk2`/`adm` fields are **Card codes.** The optional `pin1`/`puk1`/`pin2`/`puk2`/`adm` fields are
stored for later use (nothing consumes them yet). PIN/PUK must be **4–8 stored for later use (nothing consumes them yet). PIN/PUK must be **4–8
decimal digits** when set (empty is fine; anything else is refused with `400` decimal digits** when set (empty is fine; anything else is refused with `400`
naming the field); the ADM key stays free-form (hex or ASCII digits). naming the field); the ADM key is 4–16 hex digits (a decimal PIN is fine).
**Counters.** The store is the source of truth for the SCP80 counters, one per **Counters.** The store is the source of truth for the SCP80 counters, one per
keyset ("a dedicated counter shall be associated to each key version", keyset ("a dedicated counter shall be associated to each key version",
+1 -1
View File
@@ -352,7 +352,7 @@
<tbody> <tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Name</td><td class="py-1 px-2">Понятная метка (обязательна)</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Name</td><td class="py-1 px-2">Понятная метка (обязательна)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ICCID</td><td class="py-1 px-2">Опциональный идентификатор карты. Кнопка <strong>С карты</strong> подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается); при сохранении ICCID, уже присутствующий в списке, отклоняется (сравнение без пробелов и с учётом сырой hex-формы, конфликтующий пресет называется)</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ICCID</td><td class="py-1 px-2">Опциональный идентификатор карты. Кнопка <strong>С карты</strong> подставляет EF.ICCID подключённой карты (активна, только когда ICCID читается); при сохранении ICCID, уже присутствующий в списке, отклоняется (сравнение без пробелов и с учётом сырой hex-формы, конфликтующий пресет называется)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PIN1 / PUK1 / PIN2 / PUK2 / ADM</td><td class="py-1 px-2">Необязательные коды карты, сохраняются для будущего использования (пока ничем не используются). PIN/PUK — строго 4&ndash;8 десятичных цифр (иначе сохранение отклоняется); ADM — администраторский ключ (hex или до 8 ASCII-цифр), используется файловым менеджером. Представлениями SCP80/SCP81 не используются</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PIN1 / PUK1 / PIN2 / PUK2 / ADM</td><td class="py-1 px-2">Необязательные коды карты, сохраняются для будущего использования (пока ничем не используются). PIN/PUK — строго 4&ndash;8 десятичных цифр (иначе сохранение отклоняется); ADM — администраторский ключ (4&ndash;16 hex-цифр, например десятичный PIN), используется файловым менеджером. Представлениями SCP80/SCP81 не используются</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc / KID</td><td class="py-1 px-2">Индикаторы ключа и алгоритма (например, 15 = индекс 1, 3DES-CBC2; x2 = AES); задаются вместе с ключами</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc / KID</td><td class="py-1 px-2">Индикаторы ключа и алгоритма (например, 15 = индекс 1, 3DES-CBC2; x2 = AES); задаются вместе с ключами</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR / MSL / Description</td><td class="py-1 px-2">Таблица TAR: одна строка на TAR с его MSL (минимальный SPI1, TS 102 226 &sect;8.2.1.3.2.4). Первые три строки — фиксированные роли (предзаполнены, не удаляются) с фиксированными описаниями; дополнительные TAR добавляются кнопкой <strong>Добавить TAR</strong>, несут необязательное описание и могут быть удалены. SPI1 пакета берётся из MSL его TAR</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR / MSL / Description</td><td class="py-1 px-2">Таблица TAR: одна строка на TAR с его MSL (минимальный SPI1, TS 102 226 &sect;8.2.1.3.2.4). Первые три строки — фиксированные роли (предзаполнены, не удаляются) с фиксированными описаниями; дополнительные TAR добавляются кнопкой <strong>Добавить TAR</strong>, несут необязательное описание и могут быть удалены. SPI1 пакета берётся из MSL его TAR</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ISD compact format</td><td class="py-1 px-2">TAR Issuer Security Domain (TS 101 220, приложение D); используется для операций RAM/GP (представление SCP80 RAM, цепочки RAM/GP); по умолчанию по спецификации <code class="font-mono text-sm">000000</code>, подставляется при пустом значении</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ISD compact format</td><td class="py-1 px-2">TAR Issuer Security Domain (TS 101 220, приложение D); используется для операций RAM/GP (представление SCP80 RAM, цепочки RAM/GP); по умолчанию по спецификации <code class="font-mono text-sm">000000</code>, подставляется при пустом значении</td></tr>
+1 -1
View File
@@ -351,7 +351,7 @@
<tbody> <tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Name</td><td class="py-1 px-2">Human-readable label (required)</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Name</td><td class="py-1 px-2">Human-readable label (required)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ICCID</td><td class="py-1 px-2">Optional card identifier. <strong>From card</strong> fills it with the equipped card's EF.ICCID (enabled only while a readable ICCID is known); saving refuses an ICCID that already exists in the list (compared ignoring spaces and the raw-hex form, and the conflicting preset is named)</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ICCID</td><td class="py-1 px-2">Optional card identifier. <strong>From card</strong> fills it with the equipped card's EF.ICCID (enabled only while a readable ICCID is known); saving refuses an ICCID that already exists in the list (compared ignoring spaces and the raw-hex form, and the conflicting preset is named)</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PIN1 / PUK1 / PIN2 / PUK2 / ADM</td><td class="py-1 px-2">Optional card codes, all stored for later use (nothing consumes them yet). PIN/PUK must be 4&ndash;8 decimal digits (saving refuses anything else); ADM is the administrator key (hex, or up to 8 ASCII digits), used by the file manager. None is used by the SCP80/SCP81 views</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">PIN1 / PUK1 / PIN2 / PUK2 / ADM</td><td class="py-1 px-2">Optional card codes, all stored for later use (nothing consumes them yet). PIN/PUK must be 4&ndash;8 decimal digits (saving refuses anything else); ADM is the administrator key (4&ndash;16 hex digits, e.g. a decimal PIN), used by the file manager. None is used by the SCP80/SCP81 views</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc / KID</td><td class="py-1 px-2">Key and algorithm indicators (e.g. 15 = index 1, 3DES-CBC2; x2 = AES), required together with the keys</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">KIc / KID</td><td class="py-1 px-2">Key and algorithm indicators (e.g. 15 = index 1, 3DES-CBC2; x2 = AES), required together with the keys</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR / MSL / Description</td><td class="py-1 px-2">The TAR table: one uniform row per TAR with its MSL (Minimum SPI1, TS 102 226 &sect;8.2.1.3.2.4). The first three rows are the fixed role entries (pre-settled, not removable) with their fixed descriptions; further TARs are added with <strong>Add TAR</strong>, carry an optional description and can be removed. A packet's SPI1 is derived from its TAR's MSL</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">TAR / MSL / Description</td><td class="py-1 px-2">The TAR table: one uniform row per TAR with its MSL (Minimum SPI1, TS 102 226 &sect;8.2.1.3.2.4). The first three rows are the fixed role entries (pre-settled, not removable) with their fixed descriptions; further TARs are added with <strong>Add TAR</strong>, carry an optional description and can be removed. A packet's SPI1 is derived from its TAR's MSL</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ISD compact format</td><td class="py-1 px-2">Issuer Security Domain TAR (TS 101 220 Annex D); used for RAM/GP operations (SCP80 RAM view, RAM/GP chains); spec default <code class="font-mono text-sm">000000</code>, prefilled when empty</td></tr> <tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">ISD compact format</td><td class="py-1 px-2">Issuer Security Domain TAR (TS 101 220 Annex D); used for RAM/GP operations (SCP80 RAM view, RAM/GP chains); spec default <code class="font-mono text-sm">000000</code>, prefilled when empty</td></tr>
+36 -21
View File
@@ -1658,7 +1658,7 @@
</div> </div>
<div> <div>
<label for="cards-adm" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">ADM</label> <label for="cards-adm" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">ADM</label>
<input id="cards-adm" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="8" placeholder="ADM (optional)"> <input id="cards-adm" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="16" placeholder="ADM (optional)">
</div> </div>
</div> </div>
<p class="text-xs text-gray-500 dark:text-slate-400" data-l10n="All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.">All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.</p> <p class="text-xs text-gray-500 dark:text-slate-400" data-l10n="All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.">All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.</p>
@@ -11675,12 +11675,6 @@ function cardsRoleTar(preset, role) {
return (e && e.tar) || cardsRoleDefault(role); return (e && e.tar) || cardsRoleDefault(role);
} }
// The MSL of a role entry, '' when the preset carries none.
function cardsRoleMsl(preset, role) {
const e = cardsTarList(preset).find(x => x.role === role);
return (e && e.msl) || '';
}
// The TAR entry matching a value (case-insensitive), or null. // The TAR entry matching a value (case-insensitive), or null.
function cardsTarEntry(preset, tarHex) { function cardsTarEntry(preset, tarHex) {
const want = String(tarHex || '').trim().toUpperCase(); const want = String(tarHex || '').trim().toUpperCase();
@@ -11918,15 +11912,16 @@ function cardsTarRowHtml(e, i) {
} }
// Render the whole table from a preset's TAR list (roles first, then free // Render the whole table from a preset's TAR list (roles first, then free
// rows); the role rows are always shown, with the pre-settled defaults when // rows); the role rows are always shown - with the values given (a fresh form
// the list carries none. // passes the pre-settled defaults). A re-render must keep what is displayed,
// so it never invents defaults here.
function cardsTarRowsRender(entries) { function cardsTarRowsRender(entries) {
const box = document.getElementById('cards-tars'); const box = document.getElementById('cards-tars');
if (!box) return; if (!box) return;
const list = entries || []; const list = entries || [];
const roles = CARDS_TAR_ROLES.map(role => { const roles = CARDS_TAR_ROLES.map(role => {
const e = list.find(x => x.role === role) || {}; const e = list.find(x => x.role === role) || {};
return { tar: e.tar || cardsRoleDefault(role), msl: e.msl || CARDS_MSL_DEFAULT }; return { tar: e.tar || '', msl: e.msl || '' };
}); });
const free = list.filter(e => !e.role); const free = list.filter(e => !e.role);
_cardsTarCount = free.length; _cardsTarCount = free.length;
@@ -11935,13 +11930,13 @@ function cardsTarRowsRender(entries) {
} }
function cardsTarAdd() { function cardsTarAdd() {
cardsTarRowsRender(cardsTarsFromForm().concat([{ tar: '', msl: '', desc: '' }])); cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '' }]));
} }
// `i` is the table row index; the role rows are fixed and never removed. // `i` is the table row index; the role rows are fixed and never removed.
function cardsTarRemove(i) { function cardsTarRemove(i) {
if (i < CARDS_TAR_ROLES.length) return; if (i < CARDS_TAR_ROLES.length) return;
const rows = cardsTarsFromForm(); const rows = cardsTarsFromTable();
rows.splice(i, 1); rows.splice(i, 1);
cardsTarRowsRender(rows); cardsTarRowsRender(rows);
} }
@@ -11963,26 +11958,37 @@ function cardsFreeTarsFromForm() {
return out; return out;
} }
// The TAR table as preset entries: the three role entries from the first three // The TAR table exactly as displayed: the role rows as typed (an emptied field
// rows (an emptied field keeps the pre-settled default) plus the free rows. // stays empty) plus the free rows - the re-render source for Add/Remove, so an
function cardsTarsFromForm() { // emptied field is not silently refilled with its default.
function cardsTarsFromTable() {
const out = CARDS_TAR_ROLES.map((role, i) => { const out = CARDS_TAR_ROLES.map((role, i) => {
const get = id => { const get = id => {
const el = document.getElementById('cards-tr-' + i + '-' + id); const el = document.getElementById('cards-tr-' + i + '-' + id);
return el ? String(el.value).trim() : ''; return el ? String(el.value).trim() : '';
}; };
const tar = get('tar').replace(/\s/g, '').toUpperCase(); return { role: role,
const msl = get('msl').replace(/\s/g, '').toUpperCase(); tar: get('tar').replace(/\s/g, '').toUpperCase(),
return { role: role, tar: tar || cardsRoleDefault(role), msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '' };
msl: msl || CARDS_MSL_DEFAULT, desc: '' };
}); });
return out.concat(cardsFreeTarsFromForm()); return out.concat(cardsFreeTarsFromForm());
} }
// The TAR table as preset entries (the save path): an emptied role field keeps
// the pre-settled default - the three role entries are mandatory with a
// non-empty TAR and MSL.
function cardsTarsFromForm() {
return cardsTarsFromTable().map(e => e.role
? { role: e.role, tar: e.tar || cardsRoleDefault(e.role),
msl: e.msl || CARDS_MSL_DEFAULT, desc: '' }
: e);
}
// The role rows back to their pre-settled defaults (TAR + MSL 16) and no free // The role rows back to their pre-settled defaults (TAR + MSL 16) and no free
// rows. // rows.
function cardsTarsReset() { function cardsTarsReset() {
cardsTarRowsRender([]); cardsTarRowsRender(CARDS_TAR_ROLES.map(role => ({
role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '' })));
} }
// ===== Keyset selectors (SCP80 / RAM) ===== // ===== Keyset selectors (SCP80 / RAM) =====
@@ -21141,7 +21147,16 @@ function refreshDynamicI18n() {
if (isViewVisible('profiler-scan-modal')) profilerScanRefreshOptions(); if (isViewVisible('profiler-scan-modal')) profilerScanRefreshOptions();
pysimUpdateCardActionLabels(); pysimUpdateCardActionLabels();
pysimUpdateStateIndicator(); pysimUpdateStateIndicator();
if (isViewVisible('tab-cards')) { cardsRender(); cardsSetFormMode(); } if (isViewVisible('tab-cards')) {
cardsRender();
cardsSetFormMode();
// the editor's dynamic rows carry t() strings (placeholders, titles):
// rebuild them from what is typed, not from the defaults
if (isViewVisible('cards-editor-view')) {
cardsKeysetRowsRender(cardsKeysetsFromForm());
cardsTarRowsRender(cardsTarsFromTable());
}
}
if (isViewVisible('scp81-sub-scripts')) { scriptsRender(); scriptsRebuildSelect(); } if (isViewVisible('scp81-sub-scripts')) { scriptsRender(); scriptsRebuildSelect(); }
if (_ramExplorerData && isViewVisible('ram-explorer')) ramRenderExplorer(); if (_ramExplorerData && isViewVisible('ram-explorer')) ramRenderExplorer();
// the CAP analysis bodies are built with t(); the legends are static // the CAP analysis bodies are built with t(); the legends are static
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v361'; const CACHE = 'simple-v362';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+1 -1
View File
@@ -28,7 +28,7 @@ function extractFunc(src, name) {
} }
let code = ''; let code = '';
for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl', for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'cardsTarEntry', 'cardsTarMsl', 'cardsApplyFields', 'cardsApply', 'cardsTarEntry', 'cardsTarMsl', 'cardsApplyFields', 'cardsApply',
'spRefreshFromPreset', 'spPorAccepted', 'spNextCntr', 'spPresetIdx', 'spRefreshFromPreset', 'spPorAccepted', 'spNextCntr', 'spPresetIdx',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
+11 -4
View File
@@ -41,10 +41,10 @@ for (const fn of ['cardsFormValues', 'cardsClearForm', 'cardsEdit',
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate', 'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged', 'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard', 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl', 'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText', 'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText',
'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove', 'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove',
'cardsFreeTarsFromForm', 'cardsTarsFromForm', 'cardsTarsReset', 'cardsFreeTarsFromForm', 'cardsTarsFromTable', 'cardsTarsFromForm', 'cardsTarsReset',
'cardsShowList', 'cardsShowEditor', 'cardsSetFormMode', 'cardsNew', 'cardsShowList', 'cardsShowEditor', 'cardsSetFormMode', 'cardsNew',
'cardsCancelEdit']) { 'cardsCancelEdit']) {
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n'; code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
@@ -181,7 +181,6 @@ test('the preset TAR table helpers read the role entries and the MSLs', () => {
{ role: 'usimRfm', tar: 'B00001', msl: '16' }, { role: 'usimRfm', tar: 'B00001', msl: '16' },
{ tar: 'AF4D02', msl: '0A', desc: 'applet' }] }); { tar: 'AF4D02', msl: '0A', desc: 'applet' }] });
assert.strictEqual(cardsRoleTar(p, 'isd'), 'AF4D01'); assert.strictEqual(cardsRoleTar(p, 'isd'), 'AF4D01');
assert.strictEqual(cardsRoleMsl(p, 'isd'), '1A');
assert.strictEqual(cardsTarMsl(p, 'af4d02'), '0A'); assert.strictEqual(cardsTarMsl(p, 'af4d02'), '0A');
assert.strictEqual(cardsTarMsl(p, '123456'), ''); assert.strictEqual(cardsTarMsl(p, '123456'), '');
assert.strictEqual(cardsTarEntry(p, 'AF4D02').desc, 'applet'); assert.strictEqual(cardsTarEntry(p, 'AF4D02').desc, 'applet');
@@ -538,7 +537,7 @@ test('the TAR table renders the role rows and adds/removes free rows', () => {
cardsTarRowsRender([{ tar: 'AF4D01', msl: '0A', desc: 'one' }, cardsTarRowsRender([{ tar: 'AF4D01', msl: '0A', desc: 'one' },
{ tar: 'AF4D02', msl: '0A', desc: 'two' }]); { tar: 'AF4D02', msl: '0A', desc: 'two' }]);
assert.strictEqual(_cardsTarCount, 2); assert.strictEqual(_cardsTarCount, 2);
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value="000000"/); assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value=""/);
assert.match(els['cards-tars'].innerHTML, /ISD compact format/); assert.match(els['cards-tars'].innerHTML, /ISD compact format/);
assert.match(els['cards-tars'].innerHTML, /cards-tr-3-tar"[^>]*value="AF4D01"/); assert.match(els['cards-tars'].innerHTML, /cards-tr-3-tar"[^>]*value="AF4D01"/);
assert.match(els['cards-tars'].innerHTML, /cards-tr-4-tar"[^>]*value="AF4D02"/); assert.match(els['cards-tars'].innerHTML, /cards-tr-4-tar"[^>]*value="AF4D02"/);
@@ -560,6 +559,14 @@ test('the TAR table renders the role rows and adds/removes free rows', () => {
// the role rows are never removable // the role rows are never removable
cardsTarRemove(0); cardsTarRemove(0);
assert.strictEqual(_cardsTarCount, 2); assert.strictEqual(_cardsTarCount, 2);
// an emptied role field survives a re-render (no silent default refill),
// while the save path still fills the default
tarRow(els, 0, { tar: '', msl: '' });
cardsTarRemove(3);
assert.strictEqual(_cardsTarCount, 0);
assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value=""/);
assert.strictEqual(cardsTarsFromForm()[0].tar, '000000');
assert.strictEqual(cardsTarsFromForm()[0].msl, '16');
}); });
test('the list view opens the editor for a new preset and Cancel returns', () => { test('the list view opens the editor for a new preset and Cancel returns', () => {
+1 -1
View File
@@ -32,7 +32,7 @@ for (const fn of ['swapNibbles', 'encIccid', 'decIccid',
'cardsNormIccid', 'cardsFindByIccid', 'cardsFindDuplicateIccid', 'cardsNormIccid', 'cardsFindByIccid', 'cardsFindDuplicateIccid',
'cardsIccidFromCard', 'cardsAutoSelectByIccid', 'cardsRender', 'cardsIccidFromCard', 'cardsAutoSelectByIccid', 'cardsRender',
'esc', 'spKeysetList', 'spKeysetKvnOf', 'esc', 'spKeysetList', 'spKeysetKvnOf',
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl', 'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'cardsScp81Complete']) { 'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'cardsScp81Complete']) {
code += extractFunc(html, fn) + '\n'; code += extractFunc(html, fn) + '\n';
} }
+10
View File
@@ -164,6 +164,16 @@ test('the card editor groups its fields into labelled fieldsets', () => {
assert.ok(!fieldsets.some(f => f.includes('id="cards-name"'))); assert.ok(!fieldsets.some(f => f.includes('id="cards-name"')));
}); });
test('the ADM input allows the full 4-16 hex digit key', () => {
// the server's _verify_adm takes 4-16 hex digits (padded to 8 CHV bytes);
// the input must not cap it at the PIN length
const m = /<input id="cards-adm"[^>]*>/.exec(html);
assert.ok(m, 'the ADM input is missing');
const limit = /maxlength="(\d+)"/.exec(m[0]);
assert.ok(!limit || parseInt(limit[1], 10) >= 16,
'the ADM input must allow 4-16 hex digits (server _verify_adm)');
});
test('the Cards tab splits the preset list and the editor into views', () => { test('the Cards tab splits the preset list and the editor into views', () => {
assert.match(html, /id="cards-list-view"/); assert.match(html, /id="cards-list-view"/);
assert.match(html, /id="cards-editor-view" class="hidden"/); assert.match(html, /id="cards-editor-view" class="hidden"/);