feat: card-driven STATUS polling and the Poll Interval Negotiation event (v3.5.9)
- EVENT_NAMES (server + PWA) corrected against TS 102 223 v18.3.0 8.25: 0x14 is "Access technology change (multiple)" (not Change of UICC Access), 0x19 Profile container, 0x1A Void, 0x1B Secured profile container, 0x1C Poll interval negotiation, 0x20-0x22 reserved. Values the CAT spec leaves "Reserved for 3GPP" now carry the concrete TS 31.111 event name + clause (0x11 (I-)WLAN access status, 0x12 Network rejection, 0x15 CSG cell selection, 0x17 IMS registration, 0x18 Incoming IMS data, 0x1D Data connection status change, 0x1E CAG cell selection, 0x1F Slices status change). - Poll Interval Negotiation (0x1C) in the Phone tab: the form proposes a Duration (unit + interval) and the UICC's answer (TS 102 223 8.97 accepted / rejected / modified + optional Duration) is decoded and shown; a "modified" duration becomes the background poll interval. The form appears when the card subscribed to the event, like every other. - Polling emulation is card-driven: a POLL INTERVAL adopts its Duration (minutes/seconds/tenths -> 1..255 s, logged) for the background poll, is echoed in the TERMINAL RESPONSE (6.8.4) and clears a POLLING OFF suspension; POLLING OFF (6.4.14) suspends proactive polling until a new POLL INTERVAL - the manual Send STATUS button and presence detection are unaffected. /api/poll-status and /api/poll-toggle report card_disabled (+ a warning when enabling while suspended); the PWA shows it in amber. - Tests: tests/test_poll.py +7, test_proactive_names.py +3, frontend event_forms +2 and poll_ui (4). Help EN/RU, docs/api.md, AGENTS. 573 frontend / 439 Python green; version 3.5.9; sw cache simple-v262.
This commit is contained in:
@@ -493,6 +493,7 @@
|
||||
<li><strong>Location Status</strong> — выпадающий список: Normal / Limited / No service (тег <code class="font-mono text-sm">9B</code>)</li>
|
||||
<li><strong>Access Technology Change</strong> — 13 типов RAT (тег <code class="font-mono text-sm">BF</code>)</li>
|
||||
<li><strong>Network Rejection</strong> — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля местоположения (MCC, MNC, LAC, RAC, TAC), технология доступа и единый выпадающий список из 53 кодов причин (EMM, GMM, 5GMM и LU)</li>
|
||||
<li><strong>Poll Interval Negotiation</strong> (0x1C) — предлагает интервал опроса в виде Duration (единица + значение); после отправки показывается ответ UICC (принято / отклонено / изменено + длительность), а значение <em>изменено</em> становится интервалом фонового опроса (TS 102 223 §7.5.22). Форма появляется, когда карта подписана на это событие.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Отправка события использует <code class="font-mono text-sm">ENVELOPE(Event Download)</code> по TS 102 223 / TS 131 111.</p>
|
||||
|
||||
@@ -503,7 +504,7 @@
|
||||
<p class="text-sm mb-3">Блок <strong>TERMINAL PROFILE</strong> (рядом с «STATUS и опрос») содержит кнопки <strong>«Отправить»</strong> (повторная отправка, как «Спасение») и <strong>«Настроить»</strong>. В диалоге настройки: селектор пресетов (модели устройств, например профиль BIP-совместимого аппарата из этого проекта), поле hex и форма с флажком на каждый бит профиля по <strong>TS 102 223 §5.2</strong> / <strong>TS 31.111 §5.2</strong> (байты 1–39; для битов 3GPP используются имена из TS 31.111); каждый байт — вертикальный список битов в порядке спецификации (<strong>b1 — младший бит, первая строка таблицы</strong>); биты, образующие числовое поле по спецификации (байт 11 — число soft keys, байт 13 — число BIP-каналов, байты 14/15 — высота/ширина экрана, байт 16 — уменьшение ширины в меню, байт 19 — версия TIA/EIA-136, байт 24 — максимум кадров), показываются одним числовым полем на весь диапазон битов, а в подсказке байта 11 отмечено зарезервированное значение <code class="font-mono text-sm">'FF'</code>; блоки размещены фиксированными группами (байты 1–12 в 2 колонки, 13–16 в 4, 17–18 в 2, 19–21 в 3, 22–25 в 2, 26–28 в 3, 29–30 в 2, далее по одному в строке); переключение бита обновляет hex, а правка hex перерисовывает форму — поле hex основное, неизвестные байты и биты сохраняются. Кнопка <strong>«Применить»</strong> отправляет новое значение на сервер (и далее на карту), сбрасывая STK-сессию как «Спасение»; изменение хранится только в памяти (значение <code class="font-mono text-sm">--terminal-profile</code> — стартовое по умолчанию). Правила вывода характеристик устройства из TP (поколение связи, бездисплейные модемы, семейство стандартов) собраны в <code class="font-mono text-sm">docs/TERMINAL_PROFILE.md</code>.</p>
|
||||
|
||||
<h3 id="status-polling" class="text-lg font-medium mb-2">8.5 Опрос STATUS</h3>
|
||||
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1–255 с, по умолчанию 30 с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
|
||||
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1–255 с, по умолчанию 30 с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. Команда карты <strong>POLL INTERVAL</strong> обновляет интервал (TS 102 223 §6.4.6; TERMINAL RESPONSE сообщает фактически используемый интервал), а <strong>POLLING OFF</strong> приостанавливает опрос до нового POLL INTERVAL — состояние показывается янтарным цветом, при этом ручная кнопка <strong>Отправить STATUS</strong> продолжает работать (на presence detection и ручной STATUS POLLING OFF не влияет). При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
|
||||
|
||||
<h3 id="pli-dict" class="text-lg font-medium mb-2">8.6 «Конфигурация TR» — данные ответа PROVIDE LOCAL INFORMATION</h3>
|
||||
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 §8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
|
||||
|
||||
+2
-1
@@ -492,6 +492,7 @@
|
||||
<li><strong>Location Status</strong> — dropdown: Normal / Limited / No service (tag <code class="font-mono text-sm">9B</code>)</li>
|
||||
<li><strong>Access Technology Change</strong> — 13 RAT types (tag <code class="font-mono text-sm">BF</code>)</li>
|
||||
<li><strong>Network Rejection</strong> — full adaptive form: registration type (LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access technology, and a 53-cause unified rejection cause dropdown covering EMM, GMM, 5GMM and LU causes</li>
|
||||
<li><strong>Poll Interval Negotiation</strong> (0x1C) — proposes a poll interval as a Duration (unit + value); after sending, the UICC's answer is shown (accepted / rejected / modified + duration), and a <em>modified</em> duration becomes the background poll interval (TS 102 223 §7.5.22). The form appears when the card subscribed to this event.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Sending an event uses <code class="font-mono text-sm">ENVELOPE(Event Download)</code> per TS 102 223 / TS 131 111.</p>
|
||||
|
||||
@@ -502,7 +503,7 @@
|
||||
<p class="text-sm mb-3">The <strong>TERMINAL PROFILE</strong> block (next to STATUS and Polling) offers <strong>Send</strong> (re-sends it, like Rescue) and <strong>Configure</strong>. The Configure dialog has a preset selector (device models, e.g. this project's BIP-capable handset profile), a hex field and a form with one checkbox per profile bit decoded per <strong>TS 102 223 §5.2</strong> / <strong>TS 31.111 §5.2</strong> (bytes 1–39, 3GPP-defined bits use the TS 31.111 names), each byte is a vertical list of its bits in spec order (<strong>b1 is the least significant bit and the first table row</strong>); bits that form a numeric value field per the spec (byte 11 soft keys, byte 13 BIP channels, byte 14/15 screen height/width, byte 16 menu width reduction, byte 19 TIA/EIA-136 version, byte 24 max frames) are shown as a single number input for the whole bit range, with the byte 11 <code class="font-mono text-sm">'FF'</code> reserved value noted in the hint; placed in fixed column groups (bytes 1-12 in 2 columns, 13-16 in 4, 17-18 in 2, 19-21 in 3, 22-25 in 2, 26-28 in 3, 29-30 in 2, later bytes one per row); toggling a bit updates the hex and editing the hex re-renders the form — the hex field is authoritative and unknown bytes/bits are preserved. <strong>Apply</strong> sends the new value to the server (and on to the card), resetting the STK session like Rescue; the change is in-memory only (the <code class="font-mono text-sm">--terminal-profile</code> CLI value is the startup default). The rules for inferring device characteristics from a TP (radio generation, headless modems, standards family) are collected in <code class="font-mono text-sm">docs/TERMINAL_PROFILE.md</code>.</p>
|
||||
|
||||
<h3 id="status-polling" class="text-lg font-medium mb-2">8.5 STATUS polling</h3>
|
||||
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1–255 s, default 30 s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. Polling stops and card state resets if the card is removed.</p>
|
||||
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1–255 s, default 30 s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. The card's <strong>POLL INTERVAL</strong> command updates the interval (TS 102 223 §6.4.6; the TERMINAL RESPONSE reports the interval actually used), and a <strong>POLLING OFF</strong> suspends polling until a new POLL INTERVAL — the panel shows that state in amber while the manual <strong>Send STATUS</strong> button keeps working (presence detection and manual STATUS are not affected by POLLING OFF). Polling stops and card state resets if the card is removed.</p>
|
||||
|
||||
<h3 id="pli-dict" class="text-lg font-medium mb-2">8.6 TR Config — PROVIDE LOCAL INFORMATION response data</h3>
|
||||
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 §8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
|
||||
|
||||
+59
-18
@@ -1560,7 +1560,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.5.8';
|
||||
const SIMPLE_VERSION = '3.5.9';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -9208,23 +9208,34 @@ function pysimFsResetEdit() {
|
||||
}
|
||||
|
||||
// ===== proactive UICC events =====
|
||||
// Event list codings (TS 102 223 v18.3.0 8.25). Values the CAT spec leaves
|
||||
// "Reserved for 3GPP" carry the concrete 3GPP event name where TS 31.111
|
||||
// defines one (clause in parentheses), so the list stays informative.
|
||||
const EVENT_NAMES = {
|
||||
0x00: 'MT call', 0x01: 'Call connected', 0x02: 'Call disconnected',
|
||||
0x03: 'Location status', 0x04: 'User activity', 0x05: 'Idle screen available',
|
||||
0x06: 'Card reader status', 0x07: 'Language selection',
|
||||
0x08: 'Browser termination', 0x09: 'Data available',
|
||||
0x0A: 'Channel status', 0x0B: 'Access Technology Change',
|
||||
0x0A: 'Channel status', 0x0B: 'Access technology change (single)',
|
||||
0x0C: 'Display parameters changed', 0x0D: 'Local connection',
|
||||
0x0E: 'Network Search Mode Change', 0x0F: 'Browsing status',
|
||||
0x10: 'Frames Information Change', 0x11: 'I-WLAN Access Status',
|
||||
0x12: 'Network Rejection', 0x13: 'HCI Connectivity',
|
||||
0x14: 'Change of UICC Access', 0x15: 'CSG Cell Change',
|
||||
0x16: 'Contactless state request', 0x17: 'Profile Container',
|
||||
0x18: 'LTE D2D Discovery Monitoring', 0x19: 'LTE D2D Communication Monitoring',
|
||||
0x1A: 'LTE D2D Announcement Response', 0x1B: 'LTE D2D Revocation',
|
||||
0x1C: 'LTE D2D Application Port', 0x1D: 'LTE D2D Security Recovery',
|
||||
0x1E: 'Off-net Emergency Call', 0x1F: 'ECall Over IMS',
|
||||
0x20: 'EARFCN Update', 0x21: 'SCEF Channel Status',
|
||||
0x0E: 'Network search mode change', 0x0F: 'Browsing status',
|
||||
0x10: 'Frames information change',
|
||||
0x11: 'Reserved for 3GPP — (I-)WLAN access status (TS 31.111 7.5.1)',
|
||||
0x12: 'Reserved for 3GPP — Network rejection (TS 31.111 7.5.2)',
|
||||
0x13: 'HCI connectivity',
|
||||
0x14: 'Access technology change (multiple)',
|
||||
0x15: 'Reserved for 3GPP — CSG cell selection (TS 31.111 7.5.3)',
|
||||
0x16: 'Contactless state request',
|
||||
0x17: 'Reserved for 3GPP — IMS registration (TS 31.111 7.5.21)',
|
||||
0x18: 'Reserved for 3GPP — Incoming IMS data (TS 31.111 7.5.20)',
|
||||
0x19: 'Profile container', 0x1A: 'Void', 0x1B: 'Secured profile container',
|
||||
0x1C: 'Poll interval negotiation',
|
||||
0x1D: 'Reserved for 3GPP — Data connection status change (TS 31.111 7.5.25)',
|
||||
0x1E: 'Reserved for 3GPP — CAG cell selection (TS 31.111 7.5.26)',
|
||||
0x1F: 'Reserved for 3GPP — Slices status change (TS 31.111 7.5.27)',
|
||||
0x20: 'Reserved for 3GPP (future usage)',
|
||||
0x21: 'Reserved for 3GPP (future usage)',
|
||||
0x22: 'Reserved for 3GPP (future usage)',
|
||||
};
|
||||
|
||||
const REJECTION_CAUSES = [
|
||||
@@ -9380,14 +9391,27 @@ const EVENT_FORMS = {
|
||||
}
|
||||
},
|
||||
0x13: null,
|
||||
0x14: { title: 'Change of UICC Access',
|
||||
0x14: { title: 'Access technology change (multiple)',
|
||||
fields: [{ id: 'tech', label: 'Access Technology', type: 'select',
|
||||
opts: [{v:0,l:'GSM'},{v:3,l:'UTRAN'},{v:8,l:'E-UTRAN'},{v:10,l:'NG-RAN'}] }],
|
||||
build(v) { return 'BF01' + (v.tech|0).toString(16).padStart(2,'0'); }
|
||||
},
|
||||
0x15: { title: 'CSG Cell Change', note: 'not_yet' },
|
||||
0x15: { title: 'CSG cell selection', note: 'not_yet' },
|
||||
0x16: null,
|
||||
0x17: null,
|
||||
0x1C: { title: 'Poll interval negotiation',
|
||||
fields: [
|
||||
{ id: 'unit', label: 'Time unit', type: 'select',
|
||||
opts: [{v:1,l:'Seconds'},{v:0,l:'Minutes'},{v:2,l:'Tenths of seconds'}] },
|
||||
{ id: 'interval', label: 'Time interval', type: 'number', min: '1', max: '255', placeholder: '1–255 (default 30 s)' },
|
||||
],
|
||||
build(v) {
|
||||
const unit = parseInt(v.unit, 10) || 0;
|
||||
let iv = parseInt(v.interval, 10);
|
||||
if (!(iv >= 1 && iv <= 255)) iv = 30;
|
||||
return ('0402' + unit.toString(16).padStart(2, '0') + iv.toString(16).padStart(2, '0')).toUpperCase();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const PLI_QUALIFIERS = [
|
||||
@@ -9899,6 +9923,11 @@ async function pysimEventSend() {
|
||||
const resp = await pysimFetch('/api/event-send', { event_type: _eventFormType, event_data: eventData });
|
||||
if (resp.sw === '9000') {
|
||||
result.textContent = (eventData ? t('Event data sent.') : t('Event sent.')) + ' SW: ' + resp.sw;
|
||||
if (resp.negotiation) {
|
||||
let neg = t('Poll interval negotiation:') + ' ' + t(resp.negotiation.result_name);
|
||||
if (resp.negotiation.seconds) neg += ' (' + resp.negotiation.seconds + ' s)';
|
||||
result.textContent += ' · ' + neg;
|
||||
}
|
||||
result.classList.remove('hidden');
|
||||
} else {
|
||||
result.textContent = 'SW: ' + resp.sw;
|
||||
@@ -9949,11 +9978,11 @@ async function pysimPollToggle() {
|
||||
const status = await pysimFetch('/api/poll-status');
|
||||
const newEnabled = !status.enabled;
|
||||
const resp = await pysimFetch('/api/poll-toggle', { enabled: newEnabled });
|
||||
pysimUpdatePollUI(resp.enabled, resp.interval);
|
||||
pysimUpdatePollUI(resp.enabled, resp.interval, resp.card_disabled, resp.warning);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function pysimUpdatePollUI(enabled, interval) {
|
||||
function pysimUpdatePollUI(enabled, interval, cardDisabled, warning) {
|
||||
const btn = document.getElementById('pli-pause-btn');
|
||||
const stat = document.getElementById('pli-poll-stat');
|
||||
if (!btn) return;
|
||||
@@ -9966,13 +9995,18 @@ function pysimUpdatePollUI(enabled, interval) {
|
||||
btn.classList.remove('bg-emerald-600', 'text-white');
|
||||
btn.classList.add('bg-gray-200', 'dark:bg-slate-700', 'text-gray-700', 'dark:text-slate-300');
|
||||
}
|
||||
if (stat) stat.textContent = enabled ? '(' + interval + 's)' : '';
|
||||
if (stat) {
|
||||
stat.className = 'text-xs ' + (cardDisabled ? 'text-amber-600 dark:text-amber-400' : 'text-gray-400');
|
||||
if (cardDisabled) stat.textContent = t('card disabled polling (POLLING OFF)');
|
||||
else if (warning) stat.textContent = warning;
|
||||
else stat.textContent = enabled ? '(' + interval + 's)' : '';
|
||||
}
|
||||
}
|
||||
|
||||
async function pysimPollStatusInit() {
|
||||
try {
|
||||
const status = await pysimFetch('/api/poll-status');
|
||||
pysimUpdatePollUI(status.enabled, status.interval);
|
||||
pysimUpdatePollUI(status.enabled, status.interval, status.card_disabled);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -15387,6 +15421,13 @@ const LANG_RU = {
|
||||
'STK menu': 'Меню STK',
|
||||
'No menu set by the card': 'Меню не задано картой',
|
||||
'Polling': 'Опрос',
|
||||
'card disabled polling (POLLING OFF)': 'карта отключила опрос (POLLING OFF)',
|
||||
'Time unit': 'Единица времени',
|
||||
'Time interval': 'Интервал времени',
|
||||
'Poll interval negotiation:': 'Согласование интервала опроса:',
|
||||
'accepted': 'принято',
|
||||
'rejected': 'отклонено',
|
||||
'modified': 'изменено',
|
||||
'STATUS...': 'STATUS...',
|
||||
'Saving...': 'Сохранение...',
|
||||
'Saved': 'Сохранено',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v261';
|
||||
const CACHE = 'simple-v262';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -31,6 +31,7 @@ function extractFunc(src, name) {
|
||||
// Rewrite top-level const -> var so the maps leak out of sloppy-mode eval.
|
||||
eval(extractBlock('const CMD_NAMES = {', 'function cmdQualifierShort').replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'cmdQualifierShort'));
|
||||
eval(extractBlock('const EVENT_NAMES = {', 'const REJECTION_CAUSES = [').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const REJECTION_CAUSES = [', 'const EVENT_FORMS = {').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const EVENT_FORMS = {', 'const PLI_QUALIFIERS = [').replace(/^const /gm, 'var '));
|
||||
|
||||
@@ -64,6 +65,27 @@ test('cmdQualifierShort returns empty for unknown types', () => {
|
||||
assert.strictEqual(cmdQualifierShort('99', 0x01), '');
|
||||
});
|
||||
|
||||
test('EVENT_NAMES follows the pinned CAT spec and names the 3GPP events', () => {
|
||||
assert.strictEqual(EVENT_NAMES[0x0B], 'Access technology change (single)');
|
||||
assert.strictEqual(EVENT_NAMES[0x14], 'Access technology change (multiple)');
|
||||
assert.strictEqual(EVENT_NAMES[0x19], 'Profile container');
|
||||
assert.strictEqual(EVENT_NAMES[0x1C], 'Poll interval negotiation');
|
||||
for (const [v, frag] of [[0x11, '(I-)WLAN access status'], [0x12, 'Network rejection'],
|
||||
[0x15, 'CSG cell selection'], [0x17, 'IMS registration'], [0x18, 'Incoming IMS data'],
|
||||
[0x1D, 'Data connection status change'], [0x1E, 'CAG cell selection'],
|
||||
[0x1F, 'Slices status change']]) {
|
||||
assert.ok(EVENT_NAMES[v].includes(frag), '0x' + v.toString(16) + ': ' + EVENT_NAMES[v]);
|
||||
}
|
||||
});
|
||||
|
||||
test('the poll interval negotiation event builds a Duration TLV', () => {
|
||||
const build = EVENT_FORMS[0x1C].build;
|
||||
assert.strictEqual(build({ unit: '1', interval: '45' }), '0402012D');
|
||||
assert.strictEqual(build({ unit: '0', interval: '2' }), '04020002');
|
||||
assert.strictEqual(build({ unit: '2', interval: '5' }), '04020205');
|
||||
assert.strictEqual(build({ unit: '1', interval: '' }), '0402011E'); // default 30 s
|
||||
});
|
||||
|
||||
test('channel status event builds the B8 channel status TLV', () => {
|
||||
const build = EVENT_FORMS[0x0A].build;
|
||||
assert.strictEqual(EVENT_FORMS[0x0A].note, undefined);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractFunc(src, name, asyncFn) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
eval(extractFunc(html, 'pysimUpdatePollUI'));
|
||||
globalThis.t = s => s;
|
||||
|
||||
function setup() {
|
||||
const els = {
|
||||
'pli-pause-btn': { textContent: '', classList: { add: () => {}, remove: () => {} } },
|
||||
'pli-poll-stat': { textContent: '', className: 'text-xs text-gray-400' },
|
||||
};
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
return els;
|
||||
}
|
||||
|
||||
test('pysimUpdatePollUI shows the interval while polling is on', () => {
|
||||
const els = setup();
|
||||
pysimUpdatePollUI(true, 30);
|
||||
assert.strictEqual(els['pli-pause-btn'].textContent, 'ON');
|
||||
assert.strictEqual(els['pli-poll-stat'].textContent, '(30s)');
|
||||
assert.ok(!els['pli-poll-stat'].className.includes('amber'));
|
||||
});
|
||||
|
||||
test('pysimUpdatePollUI shows the card-disabled state in amber', () => {
|
||||
const els = setup();
|
||||
pysimUpdatePollUI(true, 30, true);
|
||||
assert.ok(els['pli-poll-stat'].textContent.includes('POLLING OFF'), els['pli-poll-stat'].textContent);
|
||||
assert.ok(els['pli-poll-stat'].className.includes('text-amber-600'));
|
||||
});
|
||||
|
||||
test('pysimUpdatePollUI shows a server warning', () => {
|
||||
const els = setup();
|
||||
pysimUpdatePollUI(true, 30, false, 'card disabled proactive polling');
|
||||
assert.strictEqual(els['pli-poll-stat'].textContent, 'card disabled proactive polling');
|
||||
});
|
||||
|
||||
test('pysimUpdatePollUI clears the status line when off', () => {
|
||||
const els = setup();
|
||||
pysimUpdatePollUI(false, 30);
|
||||
assert.strictEqual(els['pli-pause-btn'].textContent, 'OFF');
|
||||
assert.strictEqual(els['pli-poll-stat'].textContent, '');
|
||||
});
|
||||
Reference in New Issue
Block a user