v3.18.0: the semantic event action for test scripts
The other-applet batch: a test script can send the common event downloads semantically instead of raw hex. - events.py: server-side mirrors of the PWA's event forms for Location status (0x03), Access technology change (0x0B), Network rejection (0x12 - the LA/RA/TA object selected by the registration group, the extended EMM cause and the extended information object) and Data connection status change (0x1D - status/type/TI plus the optional cause, host-clock 8.39 date-time, location information, access technology, APN and PDP/PDN/PDU type). Fields are validated before the run (a bad value is a script error, not a step failure); `src` overrides the device-identities source (default terminal, matching the Phone forms). - The engine gains the `event` action kind; the runner builds and sends it. The PWA's action-kind list stays in sync (drift-guarded - the guard itself caught its own regex bug on the first run). - Byte parity: the vectors are generated from the PWA's own builders and pinned in tests/test_events.py; the frontend suites assert the same values. PLMN and SCTS helpers are mirrored too. - The Phone tab's event form shows the built "Event data (hex)" (updated as the fields change), so every event without a builder can be used through the raw `envelope` action. - The test-step editor offers the event select + a JSON fields textarea; malformed JSON refuses the save with a clear message. - docs/test-scripts.md (the semantic-event table, an event example, the format stamp), docs/api.md and help EN/RU updated. Tests: 812 frontend + 717 Python; sw cache simple-v381.
This commit is contained in:
@@ -563,7 +563,7 @@
|
||||
<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>. Скрипты хранятся в <strong>хранилище на сервере</strong> (<code class="font-mono text-sm">~/.pysim-simple-server/test_scripts.json</code>, <code class="font-mono text-sm">--test-scripts</code>), поэтому все браузеры работают с одним набором; экспорт/импорт JSON остаётся для резервных копий и переноса скриптов между стендами (старый экспорт из localStorage импортируется как есть).</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 или готовый защищённый пакет, опциональные переопределения номера 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> — <em>Envelope</em> (Event Download: сырой тип события + данные) или <em>Event</em> (семантические формы: Location status, Access technology change, Network rejection, Data connection status change — для остальных событий форма события на вкладке «Телефон» показывает готовый hex), <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>
|
||||
|
||||
+1
-1
@@ -562,7 +562,7 @@
|
||||
<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 the <strong>server-side store</strong> (<code class="font-mono text-sm">~/.pysim-simple-server/test_scripts.json</code>, <code class="font-mono text-sm">--test-scripts</code>), so every browser manages the same set; JSON export/import remains for backup and for moving scripts between benches (the old localStorage export imports as-is).</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, or the item's text with an exact/contains match — the text is resolved against the card's menu, which follows the applet's install parameters), <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>Actions</strong> — <em>Envelope</em> (Event Download: raw event type + data) or <em>Event</em> (the semantic forms: Location status, Access technology change, Network rejection, Data connection status change — the Phone tab's event form shows the built hex for the other events), <em>Menu Selection</em> (item id 1–255, or the item's text with an exact/contains match — the text is resolved against the card's menu, which follows the applet's install parameters), <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>
|
||||
|
||||
+87
-7
@@ -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.17.0';
|
||||
const SIMPLE_VERSION = '3.18.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -6872,7 +6872,7 @@ function decodeResponse() {
|
||||
// through /api/test/status. This UI edits the script and monitors the run;
|
||||
// SCP80 steps use the card preset matched by the equipped card's ICCID, and
|
||||
// the final SCP80 counter is written back to it.
|
||||
const TEST_ACTION_KINDS = ['envelope', 'menu-select', 'file-write', 'file-read', 'apdu', 'scp80', 'status'];
|
||||
const TEST_ACTION_KINDS = ['envelope', 'event', 'menu-select', 'file-write', 'file-read', 'apdu', 'scp80', 'status'];
|
||||
let _testScripts = null;
|
||||
let _testCurrentIdx = -1;
|
||||
let _testRunState = null;
|
||||
@@ -7001,6 +7001,13 @@ function testScriptProblem(script) {
|
||||
const problem = testMenuSelectProblem(p);
|
||||
if (problem) return at + problem;
|
||||
}
|
||||
if (s.kind === 'event') {
|
||||
if (!String(p.event || '').trim()) return at + t('event is required');
|
||||
if (p.fields !== undefined && (typeof p.fields !== 'object'
|
||||
|| p.fields === null || Array.isArray(p.fields))) {
|
||||
return at + t('event fields must be an object');
|
||||
}
|
||||
}
|
||||
if ((s.kind === 'file-write' || s.kind === 'file-read') && !String(p.path || '').trim()) return at + t('file path is empty');
|
||||
} else if (s.type === 'expect') {
|
||||
if (!String(s.command || '').trim()) return at + t('command is required');
|
||||
@@ -7056,6 +7063,10 @@ function testStepSummary(step) {
|
||||
? 'text ' + (p.mode === 'contains' ? '~' : '=') + JSON.stringify(p.text)
|
||||
: 'item=' + p.item_id);
|
||||
}
|
||||
else if (step.kind === 'event') {
|
||||
const code = parseInt(p.event, 16);
|
||||
s = 'EVENT ' + (EVENT_NAMES[code] || ('0x' + String(p.event || '?')));
|
||||
}
|
||||
else if (step.kind === 'apdu') s = 'APDU ' + (p.apdu || '?');
|
||||
else if (step.kind === 'scp80') s = 'SCP80 ' + ((p.source || (p.sp ? 'sp' : 'apdu')) === 'sp' ? (p.sp || '(secured packet)') : (p.apdu || '?')) + (p.tar ? ' TAR=' + p.tar : '') + (p.spi1 ? ' SPI1=' + p.spi1 : '') + (p.spi2 ? ' SPI2=' + p.spi2 : '');
|
||||
else if (step.kind === 'file-write') s = 'UPDATE ' + (p.path || '?') + (p.mode && p.mode !== 'auto' ? ' (' + p.mode + (p.record ? ' ' + p.record : '') + ')' : '') + ' = ' + (p.data || '');
|
||||
@@ -7317,6 +7328,19 @@ function testStepRender() {
|
||||
if (step.kind === 'envelope') {
|
||||
html += testFormRow('Event type', testFormInput('test-f-event', p.event === undefined ? 3 : p.event, '0..255'));
|
||||
html += testFormRow('Event data (hex, optional)', testFormInput('test-f-data', p.data, '9B0100'));
|
||||
} else if (step.kind === 'event') {
|
||||
// The semantic event download: the four events the server builds
|
||||
// (others go through the raw envelope action with the hex the
|
||||
// Phone form shows).
|
||||
html += testFormRow('Event', testFormSelect('test-f-evtype', p.event || '03',
|
||||
[0x03, 0x0B, 0x12, 0x1D].map(c => ({
|
||||
v: c.toString(16).toUpperCase().padStart(2, '0'),
|
||||
l: EVENT_NAMES[c]}))));
|
||||
html += testFormRow('Event fields (JSON)', '<textarea id="test-f-evfields" rows="3" ' +
|
||||
'placeholder=\'{"status": 0}\' class="w-full font-mono border border-gray-300 ' +
|
||||
'dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">' +
|
||||
esc(p.fields && Object.keys(p.fields).length ? JSON.stringify(p.fields) : '') +
|
||||
'</textarea>');
|
||||
} else if (step.kind === 'menu-select') {
|
||||
html += testFormRow('Item id (1-255)', testFormInput('test-f-item', p.item_id === undefined ? '' : p.item_id, '1..255'));
|
||||
html += testFormRow('\u2026 or item text',
|
||||
@@ -7524,6 +7548,21 @@ function testStepCollect() {
|
||||
if (id) p.item_id = id;
|
||||
}
|
||||
}
|
||||
else if (kind === 'event') {
|
||||
p.event = val('test-f-evtype') || '03';
|
||||
const raw = val('test-f-evfields');
|
||||
p.fields = {};
|
||||
if (raw) {
|
||||
try {
|
||||
const obj = JSON.parse(raw);
|
||||
if (obj && typeof obj === 'object' && !Array.isArray(obj)) p.fields = obj;
|
||||
else step._eventFieldsError = true;
|
||||
} catch (e) {
|
||||
step._eventFieldsError = true;
|
||||
}
|
||||
}
|
||||
if (!step._eventFieldsError && !raw) delete step._eventFieldsError;
|
||||
}
|
||||
else if (kind === 'file-write' || kind === 'file-read') {
|
||||
p.path = val('test-f-path');
|
||||
p.mode = val('test-f-mode') || 'auto';
|
||||
@@ -7599,6 +7638,9 @@ function testStepFormError() {
|
||||
const problem = testMenuSelectProblem(p);
|
||||
if (problem) return problem;
|
||||
}
|
||||
if (step.kind === 'event' && step._eventFieldsError) {
|
||||
return t('Event fields must be a JSON object');
|
||||
}
|
||||
if ((step.kind === 'file-write' || step.kind === 'file-read') && !p.path) return t('file path is empty');
|
||||
if (step.kind === 'status' && !(p.attempts >= 1)) return t('attempts must be at least 1');
|
||||
} else if (!step.command) {
|
||||
@@ -14963,6 +15005,34 @@ function eventFitsOneEnvelope(eventDataHex) {
|
||||
return eventEnvelopeSize(eventDataHex) <= EVENT_INNER_MAX;
|
||||
}
|
||||
|
||||
// The visible event-form values (reads the DOM; a hidden or missing field is
|
||||
// left out). Shared by the send action and the hex preview.
|
||||
function pysimEventValues() {
|
||||
const cfg = EVENT_FORMS[_eventFormType];
|
||||
const values = {};
|
||||
((cfg && cfg.fields) || []).forEach(f => {
|
||||
const el = document.getElementById('ev-f-' + f.id);
|
||||
if (!el) return;
|
||||
const wrap = el.closest ? el.closest('.event-field') : null;
|
||||
if (wrap && wrap.style.display === 'none') return;
|
||||
values[f.id] = String(el.value === undefined ? '' : el.value).trim();
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
// Build the current form's event data and show it - the exact bytes the send
|
||||
// would deliver, so a raw envelope test step can be authored by copying it.
|
||||
function pysimEventPreview() {
|
||||
const el = document.getElementById('event-send-preview');
|
||||
if (!el) return;
|
||||
const cfg = EVENT_FORMS[_eventFormType];
|
||||
let hex = '';
|
||||
if (cfg && cfg.build) {
|
||||
try { hex = cfg.build(pysimEventValues()) || ''; } catch (e) { hex = ''; }
|
||||
}
|
||||
el.value = hex ? hex.toUpperCase() : '';
|
||||
}
|
||||
|
||||
function pysimEventSendForm(eventType) {
|
||||
const modal = document.getElementById('event-send-modal');
|
||||
const title = document.getElementById('event-send-title');
|
||||
@@ -15021,7 +15091,14 @@ function pysimEventSendForm(eventType) {
|
||||
esc(t('hidden parameters:') + ' ' + avail.hidden.length + ' \u00b7 ' + t('introduced after') + ' ' +
|
||||
catReleaseLabel(eff.rel) + ' \u00b7 ' + src) + '</p>';
|
||||
}
|
||||
html += '<div class="mt-3"><label class="block mb-1 text-xs font-medium text-gray-700 dark:text-slate-300">' +
|
||||
esc(t('Event data (hex)')) + '</label>' +
|
||||
'<textarea id="event-send-preview" rows="2" readonly placeholder="\u2014" ' +
|
||||
'class="w-full font-mono border border-gray-300 dark:border-slate-600 text-xs rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea></div>';
|
||||
body.innerHTML = html;
|
||||
body.oninput = pysimEventPreview;
|
||||
body.onchange = pysimEventPreview;
|
||||
pysimEventPreview();
|
||||
btn.classList.remove('hidden');
|
||||
if (eventType === 0x12) updateNetRejectionFields();
|
||||
if (eventType === 0x02) updateCallCauseFields();
|
||||
@@ -15067,13 +15144,11 @@ async function pysimEventSend() {
|
||||
let eventSrc = (cfg && cfg.src) || null;
|
||||
|
||||
if (cfg && cfg.build) {
|
||||
const values = {};
|
||||
const values = pysimEventValues();
|
||||
let valid = true;
|
||||
cfg.fields.forEach(f => {
|
||||
const el = document.getElementById('ev-f-' + f.id);
|
||||
if (!el || el.closest('.event-field').style.display === 'none') return;
|
||||
const v = el.value.trim();
|
||||
values[f.id] = v;
|
||||
const v = values[f.id];
|
||||
if (v === undefined) return; // hidden or not rendered
|
||||
if (f.pattern && v && !new RegExp('^' + f.pattern + '$').test(v)) {
|
||||
err.textContent = t('Invalid') + ': ' + t(f.label);
|
||||
err.classList.remove('hidden');
|
||||
@@ -21030,6 +21105,11 @@ const LANG_RU = {
|
||||
'PoR': 'PoR',
|
||||
'Files': 'Файлы',
|
||||
'keyset number must be 1..15': 'номер keyset должен быть 1..15',
|
||||
'Event fields (JSON)': 'Поля события (JSON)',
|
||||
'Event fields must be a JSON object': 'Поля события должны быть объектом JSON',
|
||||
'event is required': 'нужен тип события',
|
||||
'event fields must be an object': 'поля события должны быть объектом',
|
||||
'Event data (hex)': 'Данные события (hex)',
|
||||
'Could not load the test scripts from the server': 'Не удалось загрузить тест-скрипты с сервера',
|
||||
'Could not save the test script': 'Не удалось сохранить тест-скрипт',
|
||||
'Could not delete the test script': 'Не удалось удалить тест-скрипт',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v380';
|
||||
const CACHE = 'simple-v381';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -296,3 +296,28 @@ test('the single-envelope budget is enforced by the size helper', () => {
|
||||
assert.ok(!eventFitsOneEnvelope('AB'.repeat(245)));
|
||||
assert.ok(eventFitsOneEnvelope(null));
|
||||
});
|
||||
|
||||
test('the event form previews the bytes the send would deliver', () => {
|
||||
// the preview builds from the visible fields - the same bytes the send
|
||||
// uses, so a raw-envelope test step can be authored by copying it
|
||||
const els = {
|
||||
'ev-f-status': { value: '0' },
|
||||
'ev-f-mcc': { value: '250' },
|
||||
'ev-f-mnc': { value: '01' },
|
||||
'ev-f-lac': { value: '00FF' },
|
||||
'ev-f-cell': { value: '0001' },
|
||||
'event-send-preview': { value: '' },
|
||||
};
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
globalThis._eventFormType = 0x03;
|
||||
pysimEventPreview();
|
||||
assert.strictEqual(els['event-send-preview'].value, '9B0100930752F01000FF0001');
|
||||
// a hidden field is left out of the build
|
||||
els['ev-f-mcc'].closest = () => ({ style: { display: 'none' } });
|
||||
pysimEventPreview();
|
||||
assert.strictEqual(els['event-send-preview'].value, '9B0100');
|
||||
// an event without a form leaves the preview empty
|
||||
globalThis._eventFormType = 0x99;
|
||||
pysimEventPreview();
|
||||
assert.strictEqual(els['event-send-preview'].value, '');
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ function extractFunc(src, name, asyncFn) {
|
||||
}
|
||||
|
||||
eval(extractBlock('const CMD_NAMES = {', 'function cmdQualifierShort').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const EVENT_NAMES = {', 'const REJECTION_CAUSES = [').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const TEST_ACTION_KINDS = [', 'let _testScripts').replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'testTemplate'));
|
||||
eval(extractFunc(html, 'testMenuSelectProblem'));
|
||||
@@ -44,6 +45,7 @@ eval(extractFunc(html, 'testFormSelect'));
|
||||
eval(extractFunc(html, 'testStepRender'));
|
||||
eval(extractFunc(html, 'testRenderChecks'));
|
||||
eval(extractFunc(html, 'testStepCollect'));
|
||||
eval(extractFunc(html, 'testStepFormError'));
|
||||
eval(extractFunc(html, 'testWriteBackCounter'));
|
||||
eval(extractFunc(html, 'testRunPreset'));
|
||||
eval(extractFunc(html, 'testScriptsWriteback', true));
|
||||
@@ -93,6 +95,12 @@ test('testScriptProblem accepts good scripts and names bad ones', () => {
|
||||
params: {} }] }), /item id or text/);
|
||||
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'menu-select',
|
||||
params: { item_id: 1, text: 'My menu' } }] }), /not both/);
|
||||
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
|
||||
params: {} }] }), /event is required/);
|
||||
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
|
||||
params: { event: '03', fields: [] } }] }), /object/);
|
||||
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
|
||||
params: { event: '03', fields: { status: 0 } } }] }), '');
|
||||
assert.match(testScriptProblem({
|
||||
steps: [{ type: 'action', kind: 'file-read', params: {} }] }), /file path/);
|
||||
assert.match(testScriptProblem({ steps: [{ type: 'expect' }] }), /command is required/);
|
||||
@@ -201,6 +209,27 @@ test('the menu-select editor collects the item id or the text', () => {
|
||||
assert.deepStrictEqual(_testEditStep.params, { item_id: 3 });
|
||||
});
|
||||
|
||||
test('the event step collects the type and the JSON fields', () => {
|
||||
_testEditStep = { type: 'action', kind: 'event', params: {} };
|
||||
_testEditChecks = [];
|
||||
_testEditStepIndex = 0;
|
||||
fakeForm({ 'test-step-kind': 'event', 'test-f-evtype': '12',
|
||||
'test-f-evfields': '{"reg_type":"9","cause":"2"}',
|
||||
'test-f-sw': '', 'test-f-cdata': '', 'test-f-fail': 'error' });
|
||||
testStepCollect();
|
||||
assert.deepStrictEqual(_testEditStep.params,
|
||||
{ event: '12', fields: { reg_type: '9', cause: '2' } });
|
||||
assert.strictEqual(_testEditStep._eventFieldsError, undefined);
|
||||
// invalid JSON marks the form and the save refuses with a clear message
|
||||
_testEditStep = { type: 'action', kind: 'event', params: {} };
|
||||
fakeForm({ 'test-step-kind': 'event', 'test-f-evtype': '03',
|
||||
'test-f-evfields': '{oops', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-fail': 'error' });
|
||||
assert.match(testStepFormError(), /JSON object/);
|
||||
// the summary names the event
|
||||
assert.strictEqual(testStepSummary({ type: 'action', kind: 'event',
|
||||
params: { event: '03', fields: {} } }), 'EVENT Location status');
|
||||
});
|
||||
|
||||
test('the STATUS step leaves the SW check to the server default', () => {
|
||||
_testEditStep = { type: 'action', kind: 'status', params: { attempts: 5, interval_ms: 200 } };
|
||||
fakeForm({ 'test-step-kind': 'status', 'test-f-attempts': '5', 'test-f-interval': '200',
|
||||
@@ -227,6 +256,25 @@ test('the step form renders the chosen source and no pre-filled SW', () => {
|
||||
assert.match(body, /id="test-f-sw" value=""/);
|
||||
});
|
||||
|
||||
test('the event step form renders the event select and the JSON fields', () => {
|
||||
const els = {
|
||||
'test-step-modal': { classList: { add: () => {}, remove: () => {} } },
|
||||
'test-step-title': {}, 'test-step-body': {},
|
||||
'test-step-error': { classList: { add: () => {}, remove: () => {} } },
|
||||
};
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
_testEditStep = { type: 'action', kind: 'event',
|
||||
params: { event: '12', fields: { reg_type: '9' } } };
|
||||
_testEditChecks = [];
|
||||
_testEditStepIndex = 0;
|
||||
testStepRender();
|
||||
const body = els['test-step-body'].innerHTML;
|
||||
assert.match(body, /id="test-f-evtype"/);
|
||||
assert.match(body, /<option value="12" selected>/);
|
||||
assert.match(body, /id="test-f-evfields"/);
|
||||
assert.ok(body.includes('{"reg_type":"9"}'), body);
|
||||
});
|
||||
|
||||
test('hex fields strip mask wildcards, check values keep them', () => {
|
||||
_testEditStep = { type: 'action', kind: 'apdu', params: {} };
|
||||
fakeForm({ 'test-step-kind': 'apdu', 'test-f-apdu': '80E2??',
|
||||
|
||||
Reference in New Issue
Block a user