v3.15.0: test-script fixes for applet testing - preset id, kvn, PoR and file-list checks
Three gaps blocked SCP80 applet testing from the PWA: - testRunPreset() still marshalled the pre-v3.8 flat preset fields (kic/kid/keys/counter/tar/spi1/spi2, all empty on a stored preset), so every SCP80 run failed with "SCP80 preset is incomplete: no keyset is defined". The PWA sends the matched preset's id now and the server resolves it from the store (_test_run_preset_from_body; the inline `preset` body stays for API callers), so a run always uses the stored keysets, TARs and counters. Regression tests on both sides. - testscript._normalise_params dropped the scp80 step's `kvn` (the editor exposed it and the runner and validation consumed it, so scripts silently used the preset's first keyset). kvn is validated (1-15) and passed through; the client-side problem check bounds it too. - The PoR delivered as a SEND SHORT MESSAGE (PoR-in-submit, SPI2 bit 0x20) is decoded for the expect step that fetches it: the new `por` content check asserts the response status / R-APDU SW / response data, decoded with the preceding scp80 step's context (concatenated SMS accumulate across expectations). The inline transport stays on the action (check.por / check.data). - The new `files` content check asserts the File List of a REFRESH (TS 102 223 8.18, tag 12/92) - e.g. qualifier 00 = NAA initialization with full file change notification - as an order-insensitive set of FID paths. The step editor exposes both checks; help EN/RU, docs/api.md and the new authoring reference docs/test-scripts.md document the format. Tests: 803 frontend + 680 Python; sw cache simple-v378.
This commit is contained in:
@@ -563,8 +563,8 @@
|
||||
<h3 id="test-script" class="text-lg font-medium mb-2">8.9 Тест-скрипт</h3>
|
||||
<p class="text-sm mb-2"><strong>Тест-скрипт</strong> — упорядоченный список <strong>действий</strong> и <strong>ожиданий</strong> проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP 409), фоновый опрос STATUS приостанавливается; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия <strong>Очистить отчёт</strong>. Скрипты хранятся в localStorage (<code class="font-mono text-sm">simple_tests</code>) и экспортируются/импортируются как JSON.</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Действия</strong> — <em>Envelope</em> (Event Download: тип события + данные), <em>Menu Selection</em> (item id 1–255), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения TAR/SPI1/SPI2; KIc/KID и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты, а счётчик записывается обратно после запуска) и <em>STATUS</em> (при <em>попытках</em> > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> — <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em>).</li>
|
||||
<li><strong>Ожидания</strong> — карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); <code class="font-mono text-sm">9000</code> означает «команды нет» и считается ошибкой — добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое (текст / пункт / raw), а <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
|
||||
<li><strong>Действия</strong> — <em>Envelope</em> (Event Download: тип события + данные), <em>Menu Selection</em> (item id 1–255), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а счётчик записывается обратно после запуска) и <em>STATUS</em> (при <em>попытках</em> > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> — <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em>).</li>
|
||||
<li><strong>Ожидания</strong> — карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); <code class="font-mono text-sm">9000</code> означает «команды нет» и считается ошибкой — добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка <em>PoR</em> (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 <code class="font-mono text-sm">20</code>: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80) и проверка <em>Files</em> (File List команды REFRESH, например квалификатор <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
|
||||
<li><strong>Уровни ошибок</strong> — несовпадение помечает шаг как <em>Ошибка</em> (скрипт останавливается) или <em>Предупреждение</em> (продолжает); каждая проверка показывается с ожидаемым и фактическим значением.</li>
|
||||
<li><strong>Шаблоны</strong> — Empty, <em>Обзор меню STK</em>, <em>Апплет через SCP80</em>; редактор шагов — формный, отдельные скрипты экспортируются/импортируются как JSON.</li>
|
||||
</ul>
|
||||
|
||||
+2
-2
@@ -562,8 +562,8 @@
|
||||
<h3 id="test-script" class="text-lg font-medium mb-2">8.9 Test script</h3>
|
||||
<p class="text-sm mb-2">A <strong>test script</strong> is an ordered list of <strong>actions</strong> and proactive-command <strong>expectations</strong> executed server-side on the equipped card. While a run is active the card belongs to the script: other card actions are refused (HTTP 409) and background STATUS polling is paused; the run report is kept in memory (also after reloading the page) until <strong>Clear report</strong>. Scripts live in localStorage (<code class="font-mono text-sm">simple_tests</code>) and export/import as JSON.</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Actions</strong> — <em>Envelope</em> (Event Download: event type + data), <em>Menu Selection</em> (item id 1–255), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional TAR/SPI1/SPI2 overrides; KIc/KID and the counter come from the card preset matched by the equipped card's ICCID, and the counter is written back to it after the run) and <em>STATUS</em> (with <em>attempts</em> > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards — <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>).</li>
|
||||
<li><strong>Expectations</strong> — the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a <code class="font-mono text-sm">9000</code> means “no command” and is an error — add a STATUS action when the applet delivers on poll. The command type, qualifier and contents (text / item / raw checks) are verified, and the scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
|
||||
<li><strong>Actions</strong> — <em>Envelope</em> (Event Download: event type + data), <em>Menu Selection</em> (item id 1–255), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides; the keys and the counter come from the card preset matched by the equipped card's ICCID — the run names the stored preset, so it always uses its keysets, TARs and counter — and the counter is written back to it after the run) and <em>STATUS</em> (with <em>attempts</em> > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards — <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>).</li>
|
||||
<li><strong>Expectations</strong> — the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a <code class="font-mono text-sm">9000</code> means “no command” and is an error — add a STATUS action when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, a <em>PoR</em> check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit <code class="font-mono text-sm">20</code>: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first) and a <em>Files</em> check (the File List of a REFRESH, e.g. qualifier <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). The scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
|
||||
<li><strong>Fail levels</strong> — a mismatch flags the step as <em>Error</em> (terminates the script) or <em>Warning</em> (continues); each check is reported with expected vs actual values.</li>
|
||||
<li><strong>Templates</strong> — Empty, <em>STK menu browsing</em>, <em>Applet via SCP80</em>; the step editor is form-based and individual scripts export/import as JSON.</li>
|
||||
</ul>
|
||||
|
||||
+47
-19
@@ -1877,7 +1877,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.14.2';
|
||||
const SIMPLE_VERSION = '3.15.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -6946,6 +6946,10 @@ function testScriptProblem(script) {
|
||||
const src = p.source || (p.sp ? 'sp' : 'apdu');
|
||||
const value = src === 'sp' ? p.sp : p.apdu;
|
||||
if (!String(value || '').trim()) return at + t('SCP80 needs an APDU or a secured packet');
|
||||
if (p.kvn !== undefined && String(p.kvn).trim() !== '') {
|
||||
const kvn = parseInt(String(p.kvn).trim(), 10);
|
||||
if (!(kvn >= 1 && kvn <= 15)) return at + t('keyset number must be 1..15');
|
||||
}
|
||||
}
|
||||
if (s.kind === 'menu-select' && !(p.item_id >= 1 && p.item_id <= 255)) return at + t('item id must be 1..255');
|
||||
if ((s.kind === 'file-write' || s.kind === 'file-read') && !String(p.path || '').trim()) return at + t('file path is empty');
|
||||
@@ -6968,6 +6972,14 @@ function testCheckSummary(c) {
|
||||
return s;
|
||||
}
|
||||
if (c.kind === 'raw') return 'raw ' + (c.mode === 'mask' ? '~' : '=') + (c.value || '');
|
||||
if (c.kind === 'por') {
|
||||
const parts = [];
|
||||
if (c.status) parts.push('status ' + c.status);
|
||||
if (c.sw) parts.push('SW ' + (c.sw.mode === 'mask' ? '~' : '') + c.sw.value);
|
||||
if (c.data) parts.push('data ' + (c.data.mode === 'mask' ? '~' : '') + c.data.value);
|
||||
return 'por ' + parts.join(' ');
|
||||
}
|
||||
if (c.kind === 'files') return 'files ' + (c.files || []).join(' ');
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -7330,7 +7342,8 @@ function testRenderChecks() {
|
||||
_testEditChecks.forEach((c, i) => {
|
||||
html += '<div class="flex items-start gap-2 mb-1">'
|
||||
+ testFormSelect('test-check-kind-' + i, c.kind || 'text', [
|
||||
{v: 'text', l: t('Text')}, {v: 'item', l: t('Item')}, {v: 'raw', l: t('Raw')}], 'testCheckKindChanged()');
|
||||
{v: 'text', l: t('Text')}, {v: 'item', l: t('Item')}, {v: 'raw', l: t('Raw')},
|
||||
{v: 'por', l: t('PoR')}, {v: 'files', l: t('Files')}], 'testCheckKindChanged()');
|
||||
if (c.kind === 'item') {
|
||||
html += testFormInput('test-check-id-' + i, c.id, t('id'));
|
||||
html += testFormInput('test-check-text-' + i, c.text, t('text'));
|
||||
@@ -7339,6 +7352,12 @@ function testRenderChecks() {
|
||||
} else if (c.kind === 'raw') {
|
||||
html += testFormSelect('test-check-mode-' + i, c.mode || 'mask', [{v: 'exact', l: t('Exact')}, {v: 'mask', l: t('Mask ("?" = wildcard)')}]);
|
||||
html += testFormInput('test-check-value-' + i, c.value, 'AA??');
|
||||
} else if (c.kind === 'por') {
|
||||
html += testFormInput('test-check-porstatus-' + i, c.status, 'por_ok');
|
||||
html += testFormInput('test-check-por-sw-' + i, c.sw && c.sw.value, '6A88');
|
||||
html += testFormInput('test-check-por-data-' + i, c.data && c.data.value, 'AA??');
|
||||
} else if (c.kind === 'files') {
|
||||
html += testFormInput('test-check-files-' + i, (c.files || []).join(', '), '3F007F106F3A');
|
||||
} else {
|
||||
html += testFormSelect('test-check-mode-' + i, c.mode || 'contains', [{v: 'contains', l: t('Contains')}, {v: 'exact', l: t('Exact')}]);
|
||||
html += testFormInput('test-check-value-' + i, c.value, t('text'));
|
||||
@@ -7383,6 +7402,16 @@ function testChecksCollect() {
|
||||
} else if (kind === 'raw') {
|
||||
row.mode = val('test-check-mode-' + i) || 'mask';
|
||||
row.value = val('test-check-value-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase();
|
||||
} else if (kind === 'por') {
|
||||
const status = val('test-check-porstatus-' + i);
|
||||
if (status) row.status = status;
|
||||
const sw = val('test-check-por-sw-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase();
|
||||
if (sw) row.sw = {mode: sw.includes('?') ? 'mask' : 'exact', value: sw};
|
||||
const data = val('test-check-por-data-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase();
|
||||
if (data) row.data = {mode: data.includes('?') ? 'mask' : 'exact', value: data};
|
||||
} else if (kind === 'files') {
|
||||
row.files = val('test-check-files-' + i).split(/[,\s]+/)
|
||||
.map(s => s.replace(/[^0-9a-fA-F]/g, '').toUpperCase()).filter(Boolean);
|
||||
} else {
|
||||
row.mode = val('test-check-mode-' + i) || 'contains';
|
||||
row.value = val('test-check-value-' + i);
|
||||
@@ -7450,6 +7479,8 @@ function testStepCollect() {
|
||||
step.checks = JSON.parse(JSON.stringify(_testEditChecks)).filter(c => {
|
||||
if (c.kind === 'text') return !!(c.value || '').trim();
|
||||
if (c.kind === 'item') return c.id || (c.text || '').trim();
|
||||
if (c.kind === 'por') return !!(c.status || c.sw || c.data);
|
||||
if (c.kind === 'files') return !!(c.files && c.files.length);
|
||||
return !!(c.value || '').trim();
|
||||
});
|
||||
const r = {result: val('test-f-result') || 'ok'};
|
||||
@@ -7567,34 +7598,28 @@ function testRunStepHtml(step) {
|
||||
function testRunPreset() {
|
||||
const preset = cardsMatchedPreset();
|
||||
if (!preset) return {error: t('No card preset matches the equipped card - SCP80 steps need one')};
|
||||
if (!cardsScp80Complete(preset)) return {error: t('The matched card preset has incomplete SCP80 data (KIc/KID, SPI1/SPI2, counter, keys)')};
|
||||
if (!cardsScp80Complete(preset)) return {error: t('The matched card preset has incomplete SCP80 data (a complete keyset and the role TARs with their MSLs)')};
|
||||
_testLastPresetIdx = cards.indexOf(preset);
|
||||
return {preset: {
|
||||
name: preset.name || '',
|
||||
iccid: preset.iccid || '',
|
||||
kic: String(preset.kic || '').toUpperCase(),
|
||||
kid: String(preset.kid || '').toUpperCase(),
|
||||
kicKey: String(preset.kicKey || '').replace(/\s/g, '').toUpperCase(),
|
||||
kidKey: String(preset.kidKey || '').replace(/\s/g, '').toUpperCase(),
|
||||
counter: String(preset.cntr || '').replace(/\s/g, '').toUpperCase(),
|
||||
tar: String(preset.tar || '').replace(/\s/g, '').toUpperCase(),
|
||||
spi1: String(preset.spi1 || '').replace(/\s/g, '').toUpperCase(),
|
||||
spi2: String(preset.spi2 || '').replace(/\s/g, '').toUpperCase(),
|
||||
}};
|
||||
// The run names the preset; the server resolves it from the store, so the
|
||||
// run uses the stored keysets, TARs and counters (the flat preset fields
|
||||
// this used to send no longer exist - the run failed with "no keyset").
|
||||
return {preset_id: preset.id};
|
||||
}
|
||||
|
||||
async function testRunStart() {
|
||||
const cur = testCurrent();
|
||||
const problem = testScriptProblem(cur);
|
||||
if (problem) { testRunMessage(problem, 'error'); return; }
|
||||
let preset = null;
|
||||
let presetId = null;
|
||||
if (cur.steps.some(s => s.type === 'action' && s.kind === 'scp80')) {
|
||||
const found = testRunPreset();
|
||||
if (found.error) { testRunMessage(found.error, 'error'); return; }
|
||||
preset = found.preset;
|
||||
presetId = found.preset_id;
|
||||
}
|
||||
try {
|
||||
const resp = await pysimFetch('/api/test/run', {script: cur, preset: preset});
|
||||
const body = {script: cur};
|
||||
if (presetId) body.preset_id = presetId;
|
||||
const resp = await pysimFetch('/api/test/run', body);
|
||||
if (resp.error) { testRunMessage(resp.error, 'error'); return; }
|
||||
testRenderRun(resp);
|
||||
testStartRunTimer();
|
||||
@@ -20904,6 +20929,9 @@ const LANG_RU = {
|
||||
'Contains': 'Содержит',
|
||||
'Item': 'Пункт',
|
||||
'Raw': 'Raw',
|
||||
'PoR': 'PoR',
|
||||
'Files': 'Файлы',
|
||||
'keyset number must be 1..15': 'номер keyset должен быть 1..15',
|
||||
'Test script running:': 'Выполняется тест-скрипт:',
|
||||
'Test script running': 'Выполняется тест-скрипт',
|
||||
'Preset': 'Пресет',
|
||||
@@ -20913,7 +20941,7 @@ const LANG_RU = {
|
||||
'running': 'выполняется',
|
||||
'Received': 'Получено',
|
||||
'No card preset matches the equipped card - SCP80 steps need one': 'Ни один пресет карты не соответствует подключённой карте — для шагов SCP80 он нужен',
|
||||
'The matched card preset has incomplete SCP80 data (KIc/KID, SPI1/SPI2, counter, keys)': 'У подобранного пресета неполные данные SCP80 (KIc/KID, SPI1/SPI2, счётчик, ключи)',
|
||||
'The matched card preset has incomplete SCP80 data (a complete keyset and the role TARs with their MSLs)': 'У подобранного пресета неполные данные SCP80 (полный keyset и ролевые TAR с их MSL)',
|
||||
'SCP80 counter saved to the preset:': 'Счётчик SCP80 сохранён в пресет:',
|
||||
'STK menu': 'Меню STK',
|
||||
'No menu set by the card': 'Меню не задано картой',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v377';
|
||||
const CACHE = 'simple-v378';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -44,6 +44,7 @@ eval(extractFunc(html, 'testStepRender'));
|
||||
eval(extractFunc(html, 'testRenderChecks'));
|
||||
eval(extractFunc(html, 'testStepCollect'));
|
||||
eval(extractFunc(html, 'testWriteBackCounter'));
|
||||
eval(extractFunc(html, 'testRunPreset'));
|
||||
eval(extractFunc(html, 'testChecksCollect'));
|
||||
eval('var _testEditStep = null; var _testEditChecks = []; var _testEditStepIndex = -1;'
|
||||
+ ' var _testScripts = null; var _testCurrentIdx = -1; var _testRunState = null;'
|
||||
@@ -266,3 +267,85 @@ test('the Simulator hosts the Test script pill and its wiring', () => {
|
||||
assert.match(html, /\/api\/test\/status/);
|
||||
assert.match(html, /id="test-step-modal"/);
|
||||
});
|
||||
|
||||
test('the run sends the stored preset id, not the removed flat fields', () => {
|
||||
const preset = { id: 'p1', name: 'Card 1',
|
||||
keysets: [{ kic: '15', kid: '15', kicKey: 'AA'.repeat(16),
|
||||
kidKey: 'BB'.repeat(16), cntr: '0000000A' }],
|
||||
tars: [{ role: 'isd', tar: '000000', msl: '16' }] };
|
||||
globalThis.cards = [preset];
|
||||
globalThis.cardsMatchedPreset = () => preset;
|
||||
globalThis.cardsScp80Complete = () => true;
|
||||
const found = testRunPreset();
|
||||
// the preset body used to carry flat kic/kid/... fields that no longer
|
||||
// exist on a stored preset - the server resolved an empty keyset and
|
||||
// refused every SCP80 run
|
||||
assert.deepStrictEqual(found, { preset_id: 'p1' });
|
||||
assert.strictEqual(_testLastPresetIdx, 0);
|
||||
});
|
||||
|
||||
test('the run refuses without a matched or complete preset', () => {
|
||||
globalThis.cards = [];
|
||||
globalThis.cardsMatchedPreset = () => null;
|
||||
assert.match(testRunPreset().error, /No card preset matches/);
|
||||
const preset = { id: 'p2' };
|
||||
globalThis.cards = [preset];
|
||||
globalThis.cardsMatchedPreset = () => preset;
|
||||
globalThis.cardsScp80Complete = () => false;
|
||||
assert.match(testRunPreset().error, /incomplete SCP80/);
|
||||
});
|
||||
|
||||
test('the client-side problem check bounds the keyset number', () => {
|
||||
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'scp80',
|
||||
params: { apdu: '80E2', kvn: 2 } }] }), '');
|
||||
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'scp80',
|
||||
params: { apdu: '80E2', kvn: 16 } }] }), /keyset number/);
|
||||
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'scp80',
|
||||
params: { apdu: '80E2', kvn: 0 } }] }), /keyset number/);
|
||||
});
|
||||
|
||||
test('the PoR and file-list checks render and collect', () => {
|
||||
const els = { 'test-checks': {} };
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
_testEditChecks = [{ kind: 'por', status: 'por_ok',
|
||||
sw: { mode: 'mask', value: '6A8?' }, on_fail: 'error' }];
|
||||
testRenderChecks();
|
||||
const rendered = els['test-checks'].innerHTML;
|
||||
assert.ok(rendered.includes('value="por" selected'), rendered);
|
||||
assert.match(rendered, /id="test-check-porstatus-0"/);
|
||||
assert.match(rendered, /id="test-check-por-sw-0"/);
|
||||
|
||||
const values = {
|
||||
'test-check-kind-0': 'por',
|
||||
'test-check-porstatus-0': 'por_ok',
|
||||
'test-check-por-sw-0': '6A8?',
|
||||
'test-check-por-data-0': 'aabb',
|
||||
'test-check-fail-0': 'warning',
|
||||
};
|
||||
globalThis.document = { getElementById: id => (id in values ? { value: values[id] } : null) };
|
||||
testChecksCollect();
|
||||
assert.deepStrictEqual(_testEditChecks[0], {
|
||||
kind: 'por', on_fail: 'warning', status: 'por_ok',
|
||||
sw: { mode: 'mask', value: '6A8?' },
|
||||
data: { mode: 'exact', value: 'AABB' },
|
||||
});
|
||||
|
||||
_testEditChecks = [{ kind: 'files', files: ['3F007F106F3A'], on_fail: 'error' }];
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
testRenderChecks();
|
||||
assert.match(els['test-checks'].innerHTML, /id="test-check-files-0"/);
|
||||
const values2 = { 'test-check-kind-0': 'files',
|
||||
'test-check-files-0': '3F007F106F3A, 3f002fe2', 'test-check-fail-0': 'error' };
|
||||
globalThis.document = { getElementById: id => (id in values2 ? { value: values2[id] } : null) };
|
||||
testChecksCollect();
|
||||
assert.deepStrictEqual(_testEditChecks[0],
|
||||
{ kind: 'files', on_fail: 'error', files: ['3F007F106F3A', '3F002FE2'] });
|
||||
});
|
||||
|
||||
test('the check summary names the PoR and file-list checks', () => {
|
||||
assert.strictEqual(testCheckSummary({ kind: 'por', status: 'por_ok',
|
||||
sw: { mode: 'mask', value: '6A8?' }, data: { mode: 'exact', value: 'AABB' } }),
|
||||
'por status por_ok SW ~6A8? data AABB');
|
||||
assert.strictEqual(testCheckSummary({ kind: 'files', files: ['3F002FE2'] }),
|
||||
'files 3F002FE2');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user