diff --git a/docs/api.md b/docs/api.md
index 8b2d973..3a4701f 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -667,7 +667,9 @@ keysets, TARs and counters. The inline `preset` form stays for external
callers.
**Action steps** (`type: "action"`): `kind` is `envelope` (`event`, `data`),
-`menu-select` (`item_id` 1-255, or `text` + `mode` `exact`/`contains` with an
+`event` (`event` name or hex + `fields`, optional `src` - the semantic event
+download for 0x03/0x0B/0x12/0x1D), `menu-select` (`item_id` 1-255, or `text` +
+`mode` `exact`/`contains` with an
optional `case_sensitive` - the text is resolved against the card's cached
SET UP MENU, which is refreshed whenever the card sends the menu), `file-write`
(`path`, `data`, `mode` `auto`/`binary`/`record`, `record`), `file-read` (same,
diff --git a/docs/test-scripts.md b/docs/test-scripts.md
index 6e3f0a9..638ae15 100644
--- a/docs/test-scripts.md
+++ b/docs/test-scripts.md
@@ -14,7 +14,7 @@ localStorage export imports as-is), and any API client can create or run them
**Format**: server ≥ 3.15.0 (`kvn`, `por` and `files` checks), scripts stored
server-side since 3.16.0, `menu-select` by text and the spec-order TERMINAL
-RESPONSE codings since 3.17.0.
+RESPONSE codings since 3.17.0, the semantic `event` action since 3.18.0.
## Writing a test for an applet
@@ -131,7 +131,8 @@ continue correctly.
| kind | params | notes |
|---|---|---|
-| `envelope` | `event` 0–255, `data` hex (optional) | ENVELOPE(Event Download) |
+| `envelope` | `event` 0–255, `data` hex (optional) | ENVELOPE(Event Download) with raw data |
+| `event` | `event` (name or hex), `fields` (object), optional `src` | semantic ENVELOPE(Event Download) for 0x03/0x0B/0x12/0x1D (see below) |
| `menu-select` | `item_id` 1–255 **or** `text` + `mode` `exact`/`contains` (+ `case_sensitive`), not both | ENVELOPE(Menu Selection); the text is resolved against the card's cached menu - refreshed whenever the card sends SET UP MENU, so it follows the applet's install parameters |
| `file-write` | `path`, `data`, `mode` `auto`/`binary`/`record`, `record` 1–255 | UPDATE BINARY/RECORD |
| `file-read` | `path`, `mode`, `record` | READ BINARY/RECORD; verify with `check.data` |
@@ -142,6 +143,29 @@ continue correctly.
`path` is `/`-separated: `MF` (or `3F00`) or an ADF name/AID first, then FIDs
or file names - e.g. `MF/7F20/6F07`, `ADF.USIM/EF.TEST`.
+### Semantic events (`kind: "event"`)
+
+The server builds the event objects for the events a test needs most; an
+event without a builder goes through the raw `envelope` action, whose data hex
+the Phone tab's event form shows (**Event data (hex)**, copyable).
+
+```json
+{"type": "action", "kind": "event",
+ "params": {"event": "location_status",
+ "fields": {"status": 0, "mcc": "250", "mnc": "01",
+ "lac": "00FF", "cell": "0001"}}}
+```
+
+| `event` | fields |
+|---|---|
+| `location_status` / `0x03` | `status` 0 normal / 1 limited / 2 no service; for normal service `mcc`, `mnc`, `lac`, `cell` |
+| `access_tech` / `0x0B` | `tech` (TS 102 223 8.61 coding: 0 GSM, 3 UTRAN, 8 E-UTRAN, 10 NG-RAN, …) |
+| `network_rejection` / `0x12` | `reg_type` 0–17, `access_tech`, `cause`; the location object follows the registration group - LU: `lac`, GPRS: `lac`+`rac`, EPS/5GS: `tac` (4–6 hex) + optional `ext_cause`; `mcc`/`mnc`; optional `ext_info_type` (1 CAG ID / 2 NID / 3 RedCap) + `ext_info` |
+| `data_connection` / `0x1D` | `status` 0–2, `type` 0–2, optional `cause`, `ti` (hex byte), `datetime: "now"` (host clock, 8.39), `mcc`/`mnc`/`lac`/`cell`, `tech`, `loc_status`, `apn`, `pdp_type` |
+
+`src` overrides the device-identities source (`82` terminal, `83` network);
+the default is the terminal, matching the Phone tab's forms.
+
### `check` on actions
```json
@@ -245,8 +269,25 @@ pending command the next step does not expect.
## Worked examples
-### Menu selection by text (the id varies with the install parameters)
+### Event download before an applet action
+```json
+{"name": "applet event",
+ "steps": [
+ {"type": "action", "kind": "event",
+ "params": {"event": "location_status",
+ "fields": {"status": 0, "mcc": "250", "mnc": "01",
+ "lac": "00FF", "cell": "0001"}},
+ "check": {"sw": {"mode": "mask", "value": "91??"}}},
+ {"type": "expect", "command": "ANY", "respond": {"result": "ok"}}
+ ]}
+```
+
+The event's default SW check is an exact `9000`; a mask (`91??`) is needed
+when the applet answers with a proactive command, which the next expectation
+then fetches.
+
+### Menu selection by text (the id varies with the install parameters)
```json
{"name": "applet menu",
"steps": [
@@ -333,3 +374,6 @@ PoR-in-submit (with the transport asserted by the expectation):
is checked on the action (`por`/`data`).
- `files` matches the File List as a set (order-insensitive); duplicate paths
are kept.
+- Semantic `event` actions cover four events (0x03/0x0B/0x12/0x1D); every
+ other event uses the raw `envelope` action with the data hex from the
+ Phone tab's event form.
diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index 0ad18c2..14447d7 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -563,7 +563,7 @@
8.9 Тест-скрипт
Тест-скрипт — упорядоченный список действий и ожиданий проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP 409), фоновый опрос STATUS приостанавливается; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия Очистить отчёт. Скрипты хранятся в хранилище на сервере (~/.pysim-simple-server/test_scripts.json, --test-scripts), поэтому все браузеры работают с одним набором; экспорт/импорт JSON остаётся для резервных копий и переноса скриптов между стендами (старый экспорт из localStorage импортируется как есть).
-
Действия — Envelope (Event Download: тип события + данные), Menu Selection (item id 1–255 или текст пункта с точным/частичным совпадением — текст подбирается по меню карты, которое следует за параметрами установки апплета), UPDATE/READ файла (путь + режим; чтение проверяет данные файла), сырой APDU (без авто-обработчика), SCP80 (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а счётчик записывается обратно после запуска) и STATUS (при попытках > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой ? — 91??) и проверка PoR для SCP80 (любой / без PoR / por_ok).
+
Действия — Envelope (Event Download: сырой тип события + данные) или Event (семантические формы: Location status, Access technology change, Network rejection, Data connection status change — для остальных событий форма события на вкладке «Телефон» показывает готовый hex), Menu Selection (item id 1–255 или текст пункта с точным/частичным совпадением — текст подбирается по меню карты, которое следует за параметрами установки апплета), UPDATE/READ файла (путь + режим; чтение проверяет данные файла), сырой APDU (без авто-обработчика), SCP80 (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а счётчик записывается обратно после запуска) и STATUS (при попытках > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой ? — 91??) и проверка PoR для SCP80 (любой / без PoR / por_ok).
Ожидания — карта должна объявить проактивную команду на предыдущем шаге (SW 91XX, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); 9000 означает «команды нет» и считается ошибкой — добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка PoR (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 20: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80) и проверка Files (File List команды REFRESH, например квалификатор 00 = NAA initialization with full file change notification). TERMINAL RESPONSE (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.
Уровни ошибок — несовпадение помечает шаг как Ошибка (скрипт останавливается) или Предупреждение (продолжает); каждая проверка показывается с ожидаемым и фактическим значением.
Шаблоны — Empty, Обзор меню STK, Апплет через SCP80; редактор шагов — формный, отдельные скрипты экспортируются/импортируются как JSON.
A test script is an ordered list of actions and proactive-command expectations 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 Clear report. Scripts live in the server-side store (~/.pysim-simple-server/test_scripts.json, --test-scripts), 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).
-
Actions — Envelope (Event Download: event type + data), Menu Selection (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), UPDATE/READ file (path + mode; reads verify the file data), raw APDU (no auto-handler), SCP80 (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 STATUS (with attempts > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with ? nibble wildcards — 91??) plus a PoR check for SCP80 (any / no PoR / por_ok).
+
Actions — Envelope (Event Download: raw event type + data) or Event (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), Menu Selection (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), UPDATE/READ file (path + mode; reads verify the file data), raw APDU (no auto-handler), SCP80 (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 STATUS (with attempts > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with ? nibble wildcards — 91??) plus a PoR check for SCP80 (any / no PoR / por_ok).
Expectations — the card must have announced a proactive command in the previous step (SW 91XX, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a 9000 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 PoR check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit 20: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first) and a Files check (the File List of a REFRESH, e.g. qualifier 00 = NAA initialization with full file change notification). The scripted TERMINAL RESPONSE (result, item identifier, text, extra TLVs) is part of the instruction.
Fail levels — a mismatch flags the step as Error (terminates the script) or Warning (continues); each check is reported with expected vs actual values.
Templates — Empty, STK menu browsing, Applet via SCP80; the step editor is form-based and individual scripts export/import as JSON.
diff --git a/frontend/index.html b/frontend/index.html
index 38ef19b..79ecdb6 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -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)', '');
} 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) + '';
}
+ html += '
' +
+ '
';
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': 'Не удалось удалить тест-скрипт',
diff --git a/frontend/sw.js b/frontend/sw.js
index 0ea78e6..d12273f 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v380';
+const CACHE = 'simple-v381';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/event_forms.test.js b/frontend/tests/event_forms.test.js
index 0b87745..c834eb2 100644
--- a/frontend/tests/event_forms.test.js
+++ b/frontend/tests/event_forms.test.js
@@ -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, '');
+});
diff --git a/frontend/tests/testscripts.test.js b/frontend/tests/testscripts.test.js
index 412ef89..23dc095 100644
--- a/frontend/tests/testscripts.test.js
+++ b/frontend/tests/testscripts.test.js
@@ -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, /