dana-medical-site/web/scripts/test-admin-leads-refresh.mjs

83 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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');