From 4a6a142c3d5bc6c39ce59dc06f0a4ce5d2f01400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 20 Aug 2026 21:13:41 +0300 Subject: [PATCH] Expanded Format spec fixes: REFRESH qualifiers, Error Action forms, Script Chaining flags, CR-bit tags (TS 102 226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BER_QUAL.refresh: full TS 102 223 §8.6 table (00-0A) - Error Action rebuilt as single select: Proactive (DISPLAY/PLAY TONE only) / No action (82 00) / EFRMA ref (82 01 xx) / Custom hex; drop bogus Action Type / Trigger Conditions / Retry Count UI - buildBerPcHtml(allowed): no Custom option; typeBytes tone 31 -> 20 - DISPLAY TEXT Duration control emitting 84 02 - File list tag 12 -> 92 (CR bit set) - Script Chaining flags 01/11/02/03 + keep-across-reset checkbox (RFM) - ber.test.js: exact-hex tests for all above (exchange vectors) --- frontend/help-ru.html | 12 +- frontend/help.html | 12 +- frontend/index.html | 308 +++++++++++++------------------------ frontend/tests/ber.test.js | 280 +++++++++++++++++++++++++++++++++ 4 files changed, 398 insertions(+), 214 deletions(-) create mode 100644 frontend/tests/ber.test.js diff --git a/frontend/help-ru.html b/frontend/help-ru.html index b1825ad..ddb57be 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -110,18 +110,18 @@

Сборщик Immediate Action предлагает action indicator (81/82), структурированный сборщик проактивных команд (REFRESH, DISPLAY TEXT, PLAY TONE с авто-генерацией COMPREHENSION-TLV), или ручной hex-ввод.

Error Action TLV (Tag 82)

-

Условное восстановление при ошибках для graceful rollback:

+

Восстановление при ошибках по TS 102 226 §5.2.1.3 — одна из трёх форм:

    -
  • Тип действия: Немедленное (00), отложенное (01) или условное (02) выполнение
  • -
  • Условия запуска: Сопоставление SW (любая ошибка, конкретный SW, диапазон SW, конкретная команда)
  • -
  • Действие восстановления: Action Indicator (81 session indication / 82 early response) или проактивная команда (DISPLAY TEXT, PLAY TONE, REFRESH) для уведомления или отката
  • -
  • Количество попыток: Число повторных попыток до отказа (0=без повтора)
  • +
  • Проактивная команда: набор COMPREHENSION-TLV с DISPLAY TEXT или PLAY TONE (в Error Action допустимы только эти две, TS 102 226 Table 5.9)
  • +
  • Без действия: 82 00
  • +
  • Ссылка на запись EFRMA: 82 01 <ref> с номером записи 017F
  • +
  • Произвольный hex: произвольное значение TLV

Script Chaining TLV (Tag 83)

Многопакетное выполнение скрипта с сохранением контекста:

    -
  • Флаги цепочки: Позиция First/Intermediary/Last Script (бит 0/1/2)
  • +
  • Флаги цепочки: 01 первый скрипт (удалять инфо о цепочке при сбросе), 11 первый скрипт (сохранять инфо о цепочке при сбросе, только RFM), 02 последующий скрипт (будут ещё), 03 последующий скрипт (последний)
  • Идентификатор скрипта: Корреляционный идентификатор между пакетами (1-4 байта hex, авто-инкремент подсказки)
  • Дополнительные данные: Расширенная информация цепочки (опционально hex)
  • Сохранение контекста: UICC сохраняет состояние безопасности/транзакции между пакетами
  • diff --git a/frontend/help.html b/frontend/help.html index ce3040c..8e5f6ed 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -110,18 +110,18 @@

    The Immediate Action builder offers an action indicator (81/82), a structured proactive command builder (REFRESH, DISPLAY TEXT, PLAY TONE with auto-generated COMPREHENSION-TLV objects), or a freeform hex input.

    Error Action TLV (Tag 82)

    -

    Conditional error recovery for graceful rollback:

    +

    Error recovery per TS 102 226 §5.2.1.3 — one of three forms:

      -
    • Action Type: Immediate (00), delayed (01), or conditional (02) execution
    • -
    • Trigger Conditions: SW pattern matching (any error, specific SW, SW range, specific command fails)
    • -
    • Recovery Action: Action Indicator (81 session indication / 82 early response) or Proactive Command (DISPLAY TEXT, PLAY TONE, REFRESH) to notify user or fallback
    • -
    • Retry Count: Number of retry attempts before giving up (0=no retry)
    • +
    • Proactive command: COMPREHENSION-TLV set with DISPLAY TEXT or PLAY TONE (only these two are allowed in an Error Action, TS 102 226 Table 5.9)
    • +
    • No action: 82 00
    • +
    • Reference to EFRMA record: 82 01 <ref> with record number 017F
    • +
    • Custom hex: arbitrary TLV value

    Script Chaining TLV (Tag 83)

    Multi-packet script execution with context preservation:

      -
    • Chaining Flags: First/Intermediary/Last Script position (bit 0/1/2)
    • +
    • Chaining Flags: 01 first script (delete chaining info on reset), 11 first script (keep chaining info across reset, RFM only), 02 subsequent script (more to follow), 03 subsequent script (last)
    • Script ID: Correlation identifier across packets (1-4 bytes hex, auto-increment hints provided)
    • Additional Data: Extended chaining information (optional hex)
    • Context Preservation: UICC keeps security/transaction state open across chained scripts
    • diff --git a/frontend/index.html b/frontend/index.html index 7a3aefe..24e94d5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1984,12 +1984,17 @@ function genSp() { const BER_TAGS = {'c-apdu':'22','immediate':'81','error':'82','chaining':'83'}; const BER_QUAL = { refresh: [ - ['00','Full file change notification'], - ['01','File change notification'], - ['02','UICC file change notification'], - ['03','NAA application reset'], - ['04','NAA session reset'], - ['05','UICC reset'] + ['00','NAA Initialization and Full File Change Notification'], + ['01','File Change Notification'], + ['02','NAA Initialization and File Change Notification'], + ['03','NAA Initialization'], + ['04','UICC Reset'], + ['05','NAA Application Reset (not for 2G SIM)'], + ['06','NAA Session Reset (not for 2G SIM)'], + ['07','Steering of Roaming'], + ['08','Steering of Roaming for I-WLAN'], + ['09','eUICC Profile State Change (eUICC only)'], + ['0A','Application Update (eUICC only)'] ], display: [ ['00','Normal priority'], @@ -2063,76 +2068,47 @@ function onBerTypeChange(sel) { updateBerRow(sel); } -function buildErrorActionRow(rowIdx) { - return ` -
      -
      - Action Type: - -
      - -
      -
      Trigger Conditions:
      - -
      - When: - -
      - -
      - SW Pattern: - - = - - (hex) -
      - -
      - Command #: - - (optional) -
      - -
      - Mask: FFFF -
      -
      - -
      -
      Recovery Action:
      - -
      -
      - -
      - Retry Count: - - (0=no retry) -
      -
      `; +function buildErrorActionRow() { + return `
      + Error Action: + +
      +
      `; } -function buildBerPcHtml() { +function onErrorActionTypeChange(sel) { + const body = sel.nextElementSibling; + if (sel.value === 'proactive') { + body.innerHTML = buildBerPcHtml(['display','tone']); + const pc = body.querySelector('.ber-pc'); + berPopulateDevices(pc); + onBerPcTypeChange(pc.querySelector('.ber-pc-type')); + } else if (sel.value === 'noaction') { + body.innerHTML = 'Emits 82 00 (no action)'; + } else if (sel.value === 'reference') { + body.innerHTML = `
      + EFRMA record ref: + + (01-7F) +
      `; + } else { + body.innerHTML = ``; + } + const row = sel.closest('.ber-row'); + if (row) updateBerRow(sel); +} + +function buildBerPcHtml(allowed = ['refresh','display','tone']) { + const typeOpts = {refresh:'REFRESH', display:'DISPLAY TEXT', tone:'PLAY TONE'}; return `
      # @@ -2149,25 +2125,6 @@ function buildBerPcHtml() {
      `; } -function onRecoveryTypeChange(sel) { - const body = sel.nextElementSibling; - if (sel.value === 'action') { - body.innerHTML = ``; - } else if (sel.value === 'proactive') { - body.innerHTML = buildBerPcHtml(); - const pc = body.querySelector('.ber-pc'); - berPopulateDevices(pc); - onBerPcTypeChange(pc.querySelector('.ber-pc-type')); - } else { - body.innerHTML = ''; - } - const row = sel.closest('.ber-row'); - if (row) updateBerRow(sel); -} - function onBerSubTypeChange(sel) { const body = sel.closest('.ber-sub-body') || sel.parentElement.querySelector('.ber-sub-body'); const st = sel.value; @@ -2177,9 +2134,9 @@ function onBerSubTypeChange(sel) { const type = row.querySelector('.ber-type').value; if (type === 'error') { - body.innerHTML = buildErrorActionRow(row.dataset.berIdx); - const recoveryType = body.querySelector('.error-recovery-type'); - onRecoveryTypeChange(recoveryType); + body.innerHTML = buildErrorActionRow(); + const actionType = body.querySelector('.error-action-type'); + onErrorActionTypeChange(actionType); } else { body.innerHTML = ` +
      +
      + + +
      `; } else if (type === 'tone') { fields.innerHTML = `
      @@ -2235,11 +2201,9 @@ function onBerPcTypeChange(sel) { BER_TONES.forEach(([v,l])=>{const o=document.createElement('option');o.value=v;o.textContent=v+' '+l;tsel.appendChild(o);}); } else if (type === 'refresh') { fields.innerHTML = `
      - File list (tag 12): + File list (tag 92):
      `; - } else { - fields.innerHTML = ``; } updateBerRow(sel); } @@ -2280,74 +2244,36 @@ function genBerRowValue(row) { function genErrorActionValue(row, tag) { const actionType = row.querySelector('.error-action-type'); - const triggerType = row.querySelector('.error-trigger-type'); - const maskInput = row.querySelector('.error-sw-mask'); - const valueInput = row.querySelector('.error-sw-value'); - const cmdNumInput = row.querySelector('.error-cmd-num'); - const recoveryType = row.querySelector('.error-recovery-type'); - const retryCount = row.querySelector('.error-retry-count'); - if (!actionType) return ''; + const body = row.querySelector('.error-action-body'); + if (!body) return ''; - let value = actionType.value; - - const cmdNum = cmdNumInput.value ? parseInt(cmdNumInput.value) : 0xFF; - if (triggerType.value === 'cmd-number') { - value += cmdNum.toString(16).padStart(2, '0').toUpperCase(); - } else { - value += 'FF'; - } - - let swMask = '0000'; - let swValue = '0000'; - - switch (triggerType.value) { - case 'any-sw': - swMask = '0000'; - swValue = '0000'; - break; - case 'specific-sw': - swMask = 'FFFF'; - swValue = (valueInput.value || '').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0'); - break; - case 'sw-range': - swMask = (maskInput.value || '').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0') || '6A00'; - swValue = (valueInput.value || '').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0') || '6A82'; - break; - case 'cmd-number': - swMask = '0000'; - swValue = '0000'; - break; - } - - value += swMask + swValue; - - if (recoveryType) { - const rt = recoveryType.value; - if (rt === 'proactive') { - const rv = genBerPcValue(recoveryType.parentElement, '81'); - if (rv) value += rv.slice(2); - } else if (rt === 'action') { - const a = recoveryType.parentElement.querySelector('.ber-action'); - if (a) value += '8101' + a.value; + switch (actionType.value) { + case 'proactive': + return genBerPcValue(body, tag); + case 'noaction': + return tag + '00'; + case 'reference': { + const ref = (body.querySelector('.error-ref').value || '').replace(/[^0-9a-fA-F]/g, ''); + const refNum = parseInt(ref, 16); + if (!ref || isNaN(refNum) || refNum < 0x01 || refNum > 0x7F) return ''; + return tag + '01' + refNum.toString(16).padStart(2, '0').toUpperCase(); + } + default: { + const hex = (body.querySelector('.ber-hex').value || '').replace(/[^0-9a-fA-F]/g, ''); + if (!hex) return ''; + return tag + berLenStr(hex.length / 2) + hex; } } - - const retry = parseInt(retryCount.value); - if (!isNaN(retry) && retry > 0) { - value += retry.toString(16).padStart(2, '0').toUpperCase(); - } - - return tag + berLenStr(value.length/2) + value; } function buildScriptChainingRow(rowIdx, hint = '') { return `
      -
      +
      Position: +
      @@ -2383,10 +2313,14 @@ function buildScriptChainingRow(rowIdx, hint = '') { function updateChainingFlags(rowIdx) { const first = document.querySelector(`input[name="chaining-${rowIdx}"][value="first"]`); const intermediate = document.querySelector(`input[name="chaining-${rowIdx}"][value="intermediary"]`); + const lastCheckbox = document.querySelector(`input[name="chaining-${rowIdx}-last"]`); + const keepCheckbox = document.querySelector(`input[name="chaining-${rowIdx}-keep"]`); if (first && intermediate) { if (first.checked) intermediate.checked = false; if (intermediate.checked) first.checked = false; } + if (lastCheckbox) lastCheckbox.disabled = !!(first && first.checked); + if (keepCheckbox) keepCheckbox.disabled = !!(first && !first.checked); updateBerRow(document.querySelector(`[data-ber-idx="${rowIdx}"] .ber-type`)); } @@ -2411,15 +2345,19 @@ function genScriptChainingValue(row, tag) { const firstRadio = row.querySelector(`input[name="chaining-${rowIdx}"][value="first"]`); const intermediateRadio = row.querySelector(`input[name="chaining-${rowIdx}"][value="intermediary"]`); const lastCheckbox = row.querySelector(`input[name="chaining-${rowIdx}-last"]`); + const keepCheckbox = row.querySelector(`input[name="chaining-${rowIdx}-keep"]`); const scriptIdInput = row.querySelector('.chaining-script-id'); const additionalInput = row.querySelector('.chaining-additional'); - let flags = 0; - if (firstRadio && firstRadio.checked) flags |= 0x01; - if (intermediateRadio && intermediateRadio.checked) flags |= 0x02; - if (lastCheckbox && lastCheckbox.checked) flags |= 0x04; + let flags = ''; + if (firstRadio && firstRadio.checked) { + flags = (keepCheckbox && keepCheckbox.checked) ? '11' : '01'; + } else if (intermediateRadio && intermediateRadio.checked) { + flags = (lastCheckbox && lastCheckbox.checked) ? '03' : '02'; + } + if (!flags) return ''; - let value = flags.toString(16).padStart(2, '0').toUpperCase(); + let value = flags; const scriptId = (scriptIdInput.value || '').replace(/[^0-9a-fA-F]/g, ''); if (scriptId) { @@ -2444,7 +2382,7 @@ function genBerPcValue(row, tag) { const dst = pc.querySelector('.ber-pc-dst').value; let value = '8103' + cmdNum.toString(16).padStart(2,'0').toUpperCase(); - const typeBytes = {refresh:'01',display:'21',tone:'31',custom:'00'}; + const typeBytes = {refresh:'01',display:'21',tone:'20'}; value += (typeBytes[pct]||'00') + qual; value += '8202' + src + dst; @@ -2456,7 +2394,6 @@ function genBerPcValue(row, tag) { const buf = []; for (const c of text) { const cd=c.charCodeAt(0); buf.push((cd>>8)&0xFF,cd&0xFF); } value += '8D' + berLenStr(buf.length) + buf.map(b=>b.toString(16).padStart(2,'0').toUpperCase()).join(''); - value += '04' + berLenStr(1) + '80'; } else { try { const septets = gsm7TextToSeptets(text); @@ -2468,6 +2405,14 @@ function genBerPcValue(row, tag) { } } } + const durEnable = pc.querySelector('.ber-pc-dur-enable'); + if (durEnable && durEnable.checked) { + const durVal = parseInt(pc.querySelector('.ber-pc-dur-val').value); + if (!isNaN(durVal) && durVal >= 1 && durVal <= 255) { + const durUnit = pc.querySelector('.ber-pc-dur-unit').value; + value += '84' + berLenStr(2) + durUnit + durVal.toString(16).padStart(2,'0').toUpperCase(); + } + } const alpha = (pc.querySelector('.ber-pc-alpha').value||'').replace(/[^0-9a-fA-F]/g,''); if (alpha) value += '85' + berLenStr(alpha.length/2) + alpha; } else if (pct === 'tone') { @@ -2475,10 +2420,7 @@ function genBerPcValue(row, tag) { value += '8E01' + tone; } else if (pct === 'refresh') { const flist = (pc.querySelector('.ber-pc-flist').value||'').replace(/[^0-9a-fA-F]/g,''); - if (flist) value += '12' + berLenStr(flist.length/2) + flist; - } else { - const hex = (pc.querySelector('.ber-hex').value||'').replace(/[^0-9a-fA-F]/g,''); - if (hex) value += hex; + if (flist) value += '92' + berLenStr(flist.length/2) + flist; } return tag + berLenStr(value.length/2) + value; @@ -2489,44 +2431,6 @@ function berLenStr(n) { return '81' + n.toString(16).padStart(2,'0').toUpperCase(); } -function updateSwMaskDisplay(row) { - const triggerType = row.querySelector('.error-trigger-type'); - if (!triggerType) return; - const maskInput = row.querySelector('.error-sw-mask'); - const valueInput = row.querySelector('.error-sw-value'); - const display = row.querySelector('.error-sw-mask-display'); - - let mask = '0000'; - - switch (triggerType.value) { - case 'any-sw': - mask = '0000'; - break; - case 'specific-sw': - mask = 'FFFF'; - break; - case 'sw-range': - mask = maskInput.value || '6A00'; - break; - case 'cmd-number': - mask = '0000'; - break; - } - - display.textContent = mask.toUpperCase(); -} - -document.addEventListener('DOMContentLoaded', () => { - document.addEventListener('change', (e) => { - if (e.target.classList.contains('error-trigger-type') || - e.target.classList.contains('error-sw-mask')) { - const row = e.target.closest('.ber-row'); - if (row) updateSwMaskDisplay(row); - updateBerRow(e.target); - } - }); -}); - function genBerTlv() { const fmt = document.getElementById('ber-format').value; let content = ''; diff --git a/frontend/tests/ber.test.js b/frontend/tests/ber.test.js new file mode 100644 index 0000000..bdadd12 --- /dev/null +++ b/frontend/tests/ber.test.js @@ -0,0 +1,280 @@ +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) { + 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 src.slice(m.index, i + 1); +} + +class StubEl { + constructor(value = '') { + this.value = value; + this.checked = false; + this.disabled = false; + this.children = []; + this.parentElement = null; + this.nextElementSibling = null; + } + querySelector(sel) { + return this._find(sel); + } + querySelectorAll(sel) { + const out = []; + this._findAll(sel, out); + return out; + } + _find(sel) { + for (const c of this.children) { + if (c._matches(sel)) return c; + const r = c._find(sel); + if (r) return r; + } + return null; + } + _findAll(sel, out) { + for (const c of this.children) { + if (c._matches(sel)) out.push(c); + c._findAll(sel, out); + } + } + _matches(sel) { + if (sel.startsWith('.')) return this.cls === sel.slice(1); + if (sel.startsWith('input[name="')) { + const m = sel.match(/^input\[name="([^"]+)"\]\[value="([^"]+)"\]$/); + if (m) return this.name === m[1] && this.value === m[2]; + const m2 = sel.match(/^input\[name="([^"]+)"\]$/); + if (m2) return this.name === m2[1]; + } + return false; + } +} + +function el(cls, value = '') { + const e = new StubEl(value); + e.cls = cls; + return e; +} + +function buildRow() { + const row = new StubEl(); + row.cls = 'ber-row'; + row.dataset = { berIdx: '0' }; + const body = el('error-action-body'); + const type = el('error-action-type', 'proactive'); + type.nextElementSibling = body; + body.parentElement = row; + row.children.push(type, body); + return row; +} + +function buildPcHtml(extraFields) { + // Build the .ber-pc tree expected by genBerPcValue + const pc = el('ber-pc'); + const type = el('ber-pc-type', 'display'); + const num = el('ber-pc-num', '1'); + const qual = el('ber-pc-qual', '01'); + const src = el('ber-pc-src', '81'); + const dst = el('ber-pc-dst', '82'); + const text = el('ber-pc-text', 'HI'); + const enc = el('ber-pc-enc', 'ucs2'); + const alpha = el('ber-pc-alpha', ''); + const durEnable = el('ber-pc-dur-enable'); + durEnable.checked = true; + const durUnit = el('ber-pc-dur-unit', '01'); + const durVal = el('ber-pc-dur-val', '30'); + pc.children.push(type, num, qual, src, dst, text, enc, alpha, durEnable, durUnit, durVal); + if (extraFields) extraFields(pc); + return pc; +} + +const FNS = ['berLenStr', 'gsm7TextToSeptets', 'gsm7Encode', 'genBerPcValue', 'genErrorActionValue', + 'genScriptChainingValue']; +let code = ''; +for (const f of FNS) code += extractFunc(html, f) + '\n'; +const berTagsMatch = html.match(/const BER_TAGS = \{[^}]*\};/); +const berQualMatch = html.match(/const BER_QUAL = \{[\s\S]*?\n\};/); +const berDevMatch = html.match(/const BER_DEVICES = \[[\s\S]*?\];/); +const berTonesMatch = html.match(/const BER_TONES = \[[\s\S]*?\];/); +const gsm7AlphaMatch = html.match(/const GSM7_ALPHABET = \[[\s\S]*?\n\];/); +const gsm7ExtMatch = html.match(/const GSM7_EXT_MAP = \{[\s\S]*?\n\};/); +for (const m of [berTagsMatch, berQualMatch, berDevMatch, berTonesMatch, gsm7AlphaMatch, gsm7ExtMatch]) { + if (!m) throw new Error('constant not found'); + code = m[0] + '\n' + code; +} +code += '\nvar __berConsts = {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP};'; +eval(code); +const {BER_TAGS, BER_QUAL, BER_DEVICES, BER_TONES, GSM7_ALPHABET, GSM7_EXT_MAP} = __berConsts; + +function mkRow(type) { + const row = new StubEl(); + row.cls = 'ber-row'; + row.dataset = { berIdx: '0' }; + const body = el('error-action-body'); + const actionType = el('error-action-type', type); + actionType.nextElementSibling = body; + row.children.push(actionType, body); + return row; +} + +test('BER_QUAL.refresh matches TS 102 223 §8.6 mapping', () => { + const r = BER_QUAL.refresh; + assert.strictEqual(r[0][0], '00'); + assert.strictEqual(r[0][1], 'NAA Initialization and Full File Change Notification'); + assert.strictEqual(r[4][0], '04'); + assert.strictEqual(r[4][1], 'UICC Reset'); + assert.strictEqual(r[5][1], 'NAA Application Reset (not for 2G SIM)'); + assert.strictEqual(r[6][1], 'NAA Session Reset (not for 2G SIM)'); + assert.strictEqual(r[7][1], 'Steering of Roaming'); + assert.strictEqual(r.length, 11); +}); + +test('genBerPcValue DISPLAY TEXT GSM7 "HI"', () => { + const pc = buildPcHtml(); + pc.children.find(c => c.cls === 'ber-pc-enc').value = 'gsm7'; + pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false; + const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81'); + assert.strictEqual(val, '810D8103012101820281828D02C824'); +}); + +test('genBerPcValue DISPLAY TEXT UCS2 "HI" + 30s duration', () => { + const pc = buildPcHtml(); + const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81'); + // 81 13 8103 01 21 01 8202 81 82 8D 04 0048 0049 84 02 01 1E + assert.strictEqual(val, '81138103012101820281828D04004800498402011E'); +}); + +test('genBerPcValue duration disabled omits 84', () => { + const pc = buildPcHtml(); + pc.children.find(c => c.cls === 'ber-pc-dur-enable').checked = false; + const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81'); + assert.ok(!val.includes('84')); +}); + +test('genBerPcValue PLAY TONE uses type byte 20', () => { + const pc = buildPcHtml(); + pc.children.find(c => c.cls === 'ber-pc-type').value = 'tone'; + pc.children.find(c => c.cls === 'ber-pc-qual').value = '00'; + pc.children.push(el('ber-pc-tone', '01')); + const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81'); + assert.strictEqual(val, '810C8103012000820281828E0101'); +}); + +test('genBerPcValue REFRESH uses CR-set file list tag 92', () => { + const pc = buildPcHtml(); + pc.children.find(c => c.cls === 'ber-pc-type').value = 'refresh'; + pc.children.find(c => c.cls === 'ber-pc-qual').value = '04'; + pc.children.push(el('ber-pc-flist', '3F00')); + const val = genBerPcValue({ querySelector: (s) => s === '.ber-pc' ? pc : null }, '81'); + assert.strictEqual(val, '810D81030101048202818292023F00'); +}); + +test('genErrorActionValue noaction emits 82 00', () => { + const row = mkRow('noaction'); + const val = genErrorActionValue(row, '82'); + assert.strictEqual(val, '8200'); +}); + +test('genErrorActionValue reference emits 82 01 ref', () => { + const row = mkRow('reference'); + row.children[1].children.push(el('error-ref', '05')); + const val = genErrorActionValue(row, '82'); + assert.strictEqual(val, '820105'); +}); + +test('genErrorActionValue reference rejects out-of-range ref', () => { + const row = mkRow('reference'); + row.children[1].children.push(el('error-ref', '80')); + assert.strictEqual(genErrorActionValue(row, '82'), ''); +}); + +test('genErrorActionValue proactive wraps proactive command', () => { + const row = mkRow('proactive'); + const pc = buildPcHtml(); + row.children[1].children.push(pc); + const val = genErrorActionValue(row, '82'); + // DISPLAY TEXT UCS2 "HI" + 30s inside Error Action + assert.strictEqual(val, '82138103012101820281828D04004800498402011E'); +}); + +test('genScriptChainingValue first emits 01', () => { + const row = new StubEl(); + row.dataset = { berIdx: '0' }; + const first = el('chaining-first', 'first'); + first.name = 'chaining-0'; + first.checked = true; + const interm = el('chaining-inter', 'intermediary'); + interm.name = 'chaining-0'; + const last = el('chaining-last', 'last'); + last.name = 'chaining-0-last'; + const keep = el('chaining-keep', 'keep'); + keep.name = 'chaining-0-keep'; + row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional')); + const val = genScriptChainingValue(row, '83'); + assert.strictEqual(val, '830101'); +}); + +test('genScriptChainingValue first with keep emits 11', () => { + const row = new StubEl(); + row.dataset = { berIdx: '0' }; + const first = el('chaining-first', 'first'); + first.name = 'chaining-0'; + first.checked = true; + const interm = el('chaining-inter', 'intermediary'); + interm.name = 'chaining-0'; + const last = el('chaining-last', 'last'); + last.name = 'chaining-0-last'; + const keep = el('chaining-keep', 'keep'); + keep.name = 'chaining-0-keep'; + keep.checked = true; + row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional')); + const val = genScriptChainingValue(row, '83'); + assert.strictEqual(val, '830111'); +}); + +test('genScriptChainingValue intermediary last emits 03', () => { + const row = new StubEl(); + row.dataset = { berIdx: '0' }; + const first = el('chaining-first', 'first'); + first.name = 'chaining-0'; + const interm = el('chaining-inter', 'intermediary'); + interm.name = 'chaining-0'; + interm.checked = true; + const last = el('chaining-last', 'last'); + last.name = 'chaining-0-last'; + last.checked = true; + const keep = el('chaining-keep', 'keep'); + keep.name = 'chaining-0-keep'; + row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional')); + const val = genScriptChainingValue(row, '83'); + assert.strictEqual(val, '830103'); +}); + +test('genScriptChainingValue no position emits empty', () => { + const row = new StubEl(); + row.dataset = { berIdx: '0' }; + const first = el('chaining-first', 'first'); + first.name = 'chaining-0'; + const interm = el('chaining-inter', 'intermediary'); + interm.name = 'chaining-0'; + const last = el('chaining-last', 'last'); + last.name = 'chaining-0-last'; + const keep = el('chaining-keep', 'keep'); + keep.name = 'chaining-0-keep'; + row.children.push(first, interm, last, keep, el('chaining-script-id'), el('chaining-additional')); + assert.strictEqual(genScriptChainingValue(row, '83'), ''); +}); \ No newline at end of file