// Embedded admin UI with a fake API and fictional leads; never connects to a server. import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import vm from 'node:vm'; import { parseHTML } from 'linkedom'; const html = await readFile(new URL('../../internal/api/adminui/index.html', import.meta.url), 'utf8'); const source = await readFile(new URL('../../internal/api/adminui/leads.js', import.meta.url), 'utf8'); const { document } = parseHTML(html); const $ = (selector) => document.querySelector(selector); const select = $('#ld-status'); Object.defineProperty(select, 'options', { get: () => [...select.querySelectorAll('option')] }); function selectStatus(status) { for (const option of select.options) { if (option.value === status) option.setAttribute('selected', ''); else option.removeAttribute('selected'); } } selectStatus('new'); const pending = [], notifications = [], timers = new Map(); let timerId = 0; const context = vm.createContext({ document, $, URLSearchParams, AbortController, Date, Intl, console, el: (tag, props = {}) => Object.assign(document.createElement(tag), props), api: (path, options) => new Promise((resolve, reject) => pending.push({ path, options, resolve, reject })), toast: (message, bad) => notifications.push({ message, bad }), debounce: (fn) => fn, plural: () => 'заявок', LEAD_STATUS: { new: ['Новая', 'blue'], done: ['Обработана', 'green'] }, setTimeout: (fn) => { const id = ++timerId; timers.set(id, fn); return id; }, clearTimeout: (id) => timers.delete(id), }); vm.runInContext(source + '\nglobalThis.testLeads = Leads;', context); const leads = context.testLeads; const response = (name = 'Тестовая заявка') => ({ data: [{ id: 1, created_at: '2026-10-23T10:00:00+03:00', name, phone: '+79990000000', status: 'new', form_title: 'Тестовая форма', message: 'Вымышленные данные' }], meta: { total: 1, counts: { new: 1 }, today: 1 } }); leads.offset = 60; const first = $('#ld-refresh').onclick(); assert.equal($('#ld-refresh').disabled, true, 'Refresh must visibly enter loading state'); assert.equal($('#ld-refresh').getAttribute('aria-busy'), 'true'); assert.match($('#ld-updated').textContent, /Обновляем/); assert.equal(new URLSearchParams(pending[0].path.split('?')[1]).get('offset'), '0', 'Manual refresh returns to newest results'); assert.equal(pending[0].options.cache, 'no-store'); pending[0].resolve(response()); await first; assert.match($('#ld-body').textContent, /Тестовая заявка/); assert.equal($('#ld-refresh').disabled, false); assert.match($('#ld-updated').textContent, /Обновлено/); assert.ok(notifications.some((n) => n.message === 'Список заявок обновлён')); assert.equal(timers.size, 0); // Filters win over an older request even if a transport ignores cancellation. const older = leads.load(); selectStatus('done'); const newer = leads.load(); assert.equal(pending[1].options.signal.aborted, true); pending[2].resolve(response('Свежий ответ')); await newer; pending[1].resolve(response('УСТАРЕВШИЙ ответ')); await older; assert.match($('#ld-body').textContent, /Свежий ответ/); assert.doesNotMatch($('#ld-body').textContent, /УСТАРЕВШИЙ/); // Preserve filters, but reset pagination for an explicit refresh. $('#ld-q').value = 'проверка'; $('#ld-from').value = '2026-10-01'; $('#ld-to').value = '2026-10-23'; leads.offset = 30; const refresh = $('#ld-refresh').onclick(); const params = new URLSearchParams(pending[3].path.split('?')[1]); assert.equal(params.get('status'), 'done'); assert.equal(params.get('q'), 'проверка'); assert.equal(params.get('from'), '2026-10-01'); assert.equal(params.get('to'), '2026-10-23'); pending[3].reject(new Error('Тестовая ошибка сервера')); await refresh; assert.equal($('#ld-refresh').disabled, false, 'Errors must release the button'); assert.match($('#ld-updated').textContent, /Не удалось/); assert.match($('#ld-body').textContent, /Свежий ответ/, 'Failure must not erase the previous results'); assert.ok(notifications.some((n) => n.bad && n.message.includes('Тестовая ошибка'))); const retry = $('#ld-refresh').onclick(); pending.at(-1).resolve({ data: [], meta: { total: 0, counts: {} } }); await retry; assert.match($('#ld-body').textContent, /Заявки не найдены/); assert.equal($('#ld-refresh').disabled, false); const timeout = $('#ld-refresh').onclick(); for (const fn of [...timers.values()]) fn(); pending.at(-1).reject(Object.assign(new Error('Aborted'), { name: 'AbortError' })); await timeout; assert.equal($('#ld-refresh').disabled, false, 'Timeout cannot leave refresh stuck'); assert.match($('#ld-updated').textContent, /время ожидания/i); assert.equal(timers.size, 0); console.log('PASS: actual refresh handler, loading/success/error/timeout feedback, no-store requests, first page, retained filters and stale-response protection; no live lead changes');