v1.0.178: расширенные тесты (131 шт) + чистка app.js. 53 новых теста: краевые случаи statusToHTML, applyParseResult, reconcileSelection, renderGroupCard(*), applyCompareEvent, renderCompareOpsTable, escHtml. Убран неиспользуемый SITE_URL, дубль UNZIP_URL, дубль promptEditor.
This commit is contained in:
+1
-4
@@ -4,8 +4,7 @@ var VM_API = 'https://contracts.kube5s.ru';
|
|||||||
var UPLOAD_URL = VM_API + '/upload';
|
var UPLOAD_URL = VM_API + '/upload';
|
||||||
var CONVERT_URL = VM_API + '/convert-doc';
|
var CONVERT_URL = VM_API + '/convert-doc';
|
||||||
var UNZIP_URL = VM_API + '/unzip-upload';
|
var UNZIP_URL = VM_API + '/unzip-upload';
|
||||||
var UNZIP_URL = 'https://contracts.kube5s.ru/unzip-upload';
|
// SITE_URL удалён (Фаза 4) — не использовался
|
||||||
var SITE_URL = ''; // same origin for api calls
|
|
||||||
|
|
||||||
var fileInput = document.getElementById('fileInput');
|
var fileInput = document.getElementById('fileInput');
|
||||||
var fileTable = document.getElementById('fileTable');
|
var fileTable = document.getElementById('fileTable');
|
||||||
@@ -464,8 +463,6 @@ async function showText(i) {
|
|||||||
window.closeModal = closeModal;
|
window.closeModal = closeModal;
|
||||||
window.showText = showText;
|
window.showText = showText;
|
||||||
|
|
||||||
// ── Промпт ───────────────────────────────────────────────────
|
|
||||||
var promptEditor = document.getElementById('promptEditor');
|
|
||||||
// ── Промпты (версионные) ─────────────────────────────────────
|
// ── Промпты (версионные) ─────────────────────────────────────
|
||||||
var promptRole = 'diff'; // текущая выбранная роль
|
var promptRole = 'diff'; // текущая выбранная роль
|
||||||
var promptEditor = document.getElementById('promptEditor');
|
var promptEditor = document.getElementById('promptEditor');
|
||||||
|
|||||||
+206
-4
@@ -117,6 +117,16 @@ eq(statusToHTML({ kind: 'parsed', count: 5, elapsed: '2.3' }), '<span class="sta
|
|||||||
eq(statusToHTML({ kind: 'error', text: 'тест' }), '<span class="status-err">✗ тест</span>', 'error с текстом');
|
eq(statusToHTML({ kind: 'error', text: 'тест' }), '<span class="status-err">✗ тест</span>', 'error с текстом');
|
||||||
eq(statusToHTML({ kind: 'error' }), '<span class="status-err">✗ Неизвестная ошибка</span>', 'error без текста');
|
eq(statusToHTML({ kind: 'error' }), '<span class="status-err">✗ Неизвестная ошибка</span>', 'error без текста');
|
||||||
|
|
||||||
|
// Краевые случаи
|
||||||
|
eq(statusToHTML(undefined), '', 'undefined → пусто');
|
||||||
|
eq(statusToHTML({ kind: 'uploading' }), '↑ 0%', 'uploading без pct → 0%');
|
||||||
|
eq(statusToHTML({ kind: 'uploading', pct: -5 }), '↑ -5%', 'uploading отрицательный pct');
|
||||||
|
eq(statusToHTML({ kind: 'uploading', pct: 100 }), '↑ 100%', 'uploading 100%');
|
||||||
|
eq(statusToHTML({ kind: 'parsed', count: 0 }), '<span class="status-ok">✓ 0 эл.</span>', 'parsed count=0');
|
||||||
|
eq(statusToHTML({ kind: 'parsed', count: 5, elapsed: '0' }), '<span class="status-ok">✓ 5 эл. (0с)</span>', 'parsed elapsed=0');
|
||||||
|
eq(statusToHTML({ kind: 'error', text: '' }), '<span class="status-err">✗ Неизвестная ошибка</span>', 'error с пустым текстом → дефолт');
|
||||||
|
eq(statusToHTML({ kind: 'unknown_kind' }), '', 'неизвестный kind → пусто');
|
||||||
|
|
||||||
// ── Тесты: applyParseResult ──────────────────────────────────
|
// ── Тесты: applyParseResult ──────────────────────────────────
|
||||||
console.log('\n=== applyParseResult ===');
|
console.log('\n=== applyParseResult ===');
|
||||||
|
|
||||||
@@ -149,6 +159,25 @@ applyParseResult(e4, null);
|
|||||||
eq(e4.status.kind, 'error', 'null: kind=error');
|
eq(e4.status.kind, 'error', 'null: kind=error');
|
||||||
eq(e4.status.text, 'Неизвестная ошибка', 'null: дефолтный текст');
|
eq(e4.status.text, 'Неизвестная ошибка', 'null: дефолтный текст');
|
||||||
|
|
||||||
|
// parsed с пустыми полями
|
||||||
|
var e5 = {};
|
||||||
|
applyParseResult(e5, { status: 'parsed' });
|
||||||
|
eq(e5.status.kind, 'parsed', 'parsed без element_count: kind=parsed');
|
||||||
|
eq(e5.status.count, undefined, 'parsed без element_count: count=undefined');
|
||||||
|
// parsed с element_count=0
|
||||||
|
var e6 = {};
|
||||||
|
applyParseResult(e6, { status: 'parsed', element_count: 0 });
|
||||||
|
eq(e6.status.count, 0, 'parsed element_count=0');
|
||||||
|
// error без текста ошибки
|
||||||
|
var e7 = {};
|
||||||
|
applyParseResult(e7, { status: 'error' });
|
||||||
|
eq(e7.status.text, 'ошибка парсинга', 'error без текста: дефолт "ошибка парсинга"');
|
||||||
|
// entry с уже существующими полями (не должны мешать)
|
||||||
|
var e8 = { existing: true, parsed: false };
|
||||||
|
applyParseResult(e8, { status: 'parsed', element_count: 7 }, '3.0');
|
||||||
|
assert(e8.existing === true, 'старое поле не затёрто');
|
||||||
|
eq(e8.status.count, 7, 'новые данные поверх старых');
|
||||||
|
|
||||||
// ── Тесты: reconcileSelection ────────────────────────────────
|
// ── Тесты: reconcileSelection ────────────────────────────────
|
||||||
console.log('\n=== reconcileSelection ===');
|
console.log('\n=== reconcileSelection ===');
|
||||||
|
|
||||||
@@ -166,6 +195,23 @@ eq(result[1].name, 'c.docx', 'c.docx остался');
|
|||||||
// Исходный массив не мутирован
|
// Исходный массив не мутирован
|
||||||
eq(files.length, 3, 'исходный массив не мутирован');
|
eq(files.length, 3, 'исходный массив не мутирован');
|
||||||
|
|
||||||
|
// Пустые входы
|
||||||
|
var emptyResult = reconcileSelection([], []);
|
||||||
|
eq(emptyResult.length, 0, 'пустые массивы → пусто');
|
||||||
|
|
||||||
|
// Все совпадают
|
||||||
|
var allMatch = reconcileSelection([{name:'a'}], [{name:'a'}]);
|
||||||
|
eq(allMatch.length, 1, 'все совпадают → 1');
|
||||||
|
|
||||||
|
// Ни один не совпадает
|
||||||
|
var noMatch = reconcileSelection([{name:'a'},{name:'b'}], [{name:'c'},{name:'d'}]);
|
||||||
|
eq(noMatch.length, 0, 'нет совпадений → пусто');
|
||||||
|
|
||||||
|
// Дубликаты имён в newFiles (должен работать — Set)
|
||||||
|
var dupNames = [{name:'a'}, {name:'a'}, {name:'b'}];
|
||||||
|
var dupResult = reconcileSelection([{name:'a'},{name:'b'}], dupNames);
|
||||||
|
eq(dupResult.length, 2, 'дубликаты в newFiles — Set обрабатывает');
|
||||||
|
|
||||||
// ── Тесты: renderGroupCard ───────────────────────────────────
|
// ── Тесты: renderGroupCard ───────────────────────────────────
|
||||||
console.log('\n=== renderGroupCard ===');
|
console.log('\n=== renderGroupCard ===');
|
||||||
|
|
||||||
@@ -199,6 +245,22 @@ var groupRunning = {
|
|||||||
var htmlRunning = renderGroupCard(groupRunning, 1);
|
var htmlRunning = renderGroupCard(groupRunning, 1);
|
||||||
contains(htmlRunning, 'disabled', 'кнопка disabled при running');
|
contains(htmlRunning, 'disabled', 'кнопка disabled при running');
|
||||||
|
|
||||||
|
// Без контрагента
|
||||||
|
var groupNoCP = { contract_number: '001', documents: [] };
|
||||||
|
var htmlNoCP = renderGroupCard(groupNoCP, 5);
|
||||||
|
contains(htmlNoCP, 'контрагент не определён', 'без counterparty: заглушка');
|
||||||
|
|
||||||
|
// Неизвестный doc_type
|
||||||
|
var groupUnknown = { contract_number: 'X', counterparty: 'Y', documents: [{ doc_type: 'unknown_type', filename: 'f.docx' }] };
|
||||||
|
var htmlUnknown = renderGroupCard(groupUnknown, 6);
|
||||||
|
contains(htmlUnknown, 'unknown_type', 'неизвестный тип: выводится как есть');
|
||||||
|
|
||||||
|
// Пустой список документов
|
||||||
|
var groupEmpty = { contract_number: 'E', counterparty: 'Empty', documents: [] };
|
||||||
|
var htmlEmpty = renderGroupCard(groupEmpty, 7);
|
||||||
|
contains(htmlEmpty, 'Договор №E', 'пустые документы: карточка рендерится');
|
||||||
|
contains(htmlEmpty, 'Сравнить', 'пустые документы: кнопка есть');
|
||||||
|
|
||||||
// ── Тесты: renderGroupCardDone ───────────────────────────────
|
// ── Тесты: renderGroupCardDone ───────────────────────────────
|
||||||
console.log('\n=== renderGroupCardDone ===');
|
console.log('\n=== renderGroupCardDone ===');
|
||||||
|
|
||||||
@@ -277,6 +339,23 @@ applyCompareEvent(sections, { type: 'extract_error', supplement_id: 's2', filena
|
|||||||
eq(sections['s2'].status, 'error', 'новая секция с ошибкой');
|
eq(sections['s2'].status, 'error', 'новая секция с ошибкой');
|
||||||
eq(sections['s2'].filename, 'bad.docx', 'filename для ошибочной секции');
|
eq(sections['s2'].filename, 'bad.docx', 'filename для ошибочной секции');
|
||||||
|
|
||||||
|
// Неизвестный тип события — не должен падать
|
||||||
|
applyCompareEvent(sections, { type: 'unknown_type', supplement_id: 's3' });
|
||||||
|
assert(sections['s3'] === undefined, 'неизвестный тип: секция НЕ создана');
|
||||||
|
|
||||||
|
// llm_done для несуществующей секции — не должен падать
|
||||||
|
applyCompareEvent(sections, { type: 'llm_done', supplement_id: 's99', ops_count: 1 });
|
||||||
|
|
||||||
|
// apply_error (должен работать как extract_error)
|
||||||
|
applyCompareEvent(sections, { type: 'apply_error', supplement_id: 's4', filename: 'apply_err.docx', error: 'apply boom' });
|
||||||
|
eq(sections['s4'].status, 'error', 'apply_error: секция с ошибкой создана');
|
||||||
|
eq(sections['s4'].error, 'apply boom', 'apply_error: текст ошибки');
|
||||||
|
|
||||||
|
// applied без summary
|
||||||
|
applyCompareEvent(sections, { type: 'applied', supplement_id: 's1', ops: [] });
|
||||||
|
eq(sections['s1'].status, 'applied', 'applied без summary: status ок');
|
||||||
|
eq(sections['s1'].ops.length, 0, 'applied с пустыми ops');
|
||||||
|
|
||||||
// ── Тесты: renderCompareSectionHeader ────────────────────────
|
// ── Тесты: renderCompareSectionHeader ────────────────────────
|
||||||
console.log('\n=== renderCompareSectionHeader ===');
|
console.log('\n=== renderCompareSectionHeader ===');
|
||||||
|
|
||||||
@@ -300,6 +379,21 @@ contains(tableHtml, 'Стойка', 'имя услуги');
|
|||||||
contains(tableHtml, '100', 'цена');
|
contains(tableHtml, '100', 'цена');
|
||||||
contains(tableHtml, '2024-01-01', 'дата');
|
contains(tableHtml, '2024-01-01', 'дата');
|
||||||
|
|
||||||
|
// Пустые ops
|
||||||
|
var emptyTable = renderCompareOpsTable([]);
|
||||||
|
contains(emptyTable, '<table', 'пустые ops: таблица рендерится');
|
||||||
|
contains(emptyTable, '<tbody>', 'пустые ops: tbody есть');
|
||||||
|
|
||||||
|
// ops с null new_row
|
||||||
|
var nullRow = renderCompareOpsTable([{ action: 'ADD' }]);
|
||||||
|
contains(nullRow, '<td></td>', 'null new_row: пустые ячейки (не падает)');
|
||||||
|
|
||||||
|
// ops с неизвестным action (без класса)
|
||||||
|
var unknownAct = renderCompareOpsTable([{ action: 'MERGE', new_row: {} }]);
|
||||||
|
assert(unknownAct.indexOf('diff-added') === -1, 'MERGE: нет diff-added');
|
||||||
|
assert(unknownAct.indexOf('diff-deleted') === -1, 'MERGE: нет diff-deleted');
|
||||||
|
assert(unknownAct.indexOf('diff-changed') === -1, 'MERGE: нет diff-changed');
|
||||||
|
|
||||||
// ── Тесты: renderCompareSectionBody ──────────────────────────
|
// ── Тесты: renderCompareSectionBody ──────────────────────────
|
||||||
console.log('\n=== renderCompareSectionBody ===');
|
console.log('\n=== renderCompareSectionBody ===');
|
||||||
|
|
||||||
@@ -319,12 +413,120 @@ contains(bodyHtml, '-0', 'deleted=0');
|
|||||||
contains(bodyHtml, '?2', 'unresolved=2');
|
contains(bodyHtml, '?2', 'unresolved=2');
|
||||||
contains(bodyHtml, 'diff-changed', 'UPDATE → diff-changed');
|
contains(bodyHtml, 'diff-changed', 'UPDATE → diff-changed');
|
||||||
|
|
||||||
// ── Итоги ────────────────────────────────────────────────────
|
// Без unresolved
|
||||||
|
var secNoUnresolved = { status: 'applied', summary: { added: 1, updated: 0, deleted: 0 }, ops: [] };
|
||||||
|
var bodyNoUnr = renderCompareSectionBody(secNoUnresolved);
|
||||||
|
assert(bodyNoUnr.indexOf('?') === -1, 'без unresolved: нет знака ?');
|
||||||
|
|
||||||
|
// Отрицательные значения summary (не должно быть, но не падать)
|
||||||
|
var secNeg = { status: 'applied', summary: { added: -1, updated: 0, deleted: 0 }, ops: [{ action: 'ADD', new_row: {} }] };
|
||||||
|
var bodyNeg = renderCompareSectionBody(secNeg);
|
||||||
|
contains(bodyNeg, '+-1', 'отрицательные summary: рендерится без ошибок');
|
||||||
|
|
||||||
|
// ops с частичным new_row (не все поля)
|
||||||
|
var secPartial = { status: 'applied', summary: { added: 1 }, ops: [
|
||||||
|
{ action: 'ADD', new_row: { name: 'Только имя' } }
|
||||||
|
]};
|
||||||
|
var bodyPartial = renderCompareSectionBody(secPartial);
|
||||||
|
contains(bodyPartial, 'Только имя', 'частичный new_row: имя есть');
|
||||||
|
// Проверим что нет undefined в выводе
|
||||||
|
assert(bodyPartial.indexOf('undefined') === -1, 'нет undefined в выводе');
|
||||||
|
|
||||||
|
// ── Тесты: escHtml ───────────────────────────────────────────
|
||||||
|
console.log('\n=== escHtml ===');
|
||||||
|
|
||||||
|
eq(escHtml(null), '', 'escHtml null → пусто');
|
||||||
|
eq(escHtml(undefined), '', 'escHtml undefined → пусто');
|
||||||
|
eq(escHtml('hello'), 'hello', 'escHtml обычный текст');
|
||||||
|
eq(escHtml('<script>'), '<script>', 'escHtml экранирует < >');
|
||||||
|
eq(escHtml('a&b'), 'a&b', 'escHtml экранирует &');
|
||||||
|
eq(escHtml('"quote"'), '"quote"', 'escHtml экранирует кавычки');
|
||||||
|
eq(escHtml(123), '123', 'escHtml число → строка');
|
||||||
|
|
||||||
|
// ── Дымные тесты API (бэкенд) ────────────────────────────────
|
||||||
|
console.log('\n=== API smoke tests ===');
|
||||||
|
|
||||||
|
var http = require('http');
|
||||||
|
var https = require('https');
|
||||||
|
|
||||||
|
function apiTest(method, url, cb) {
|
||||||
|
var mod = url.startsWith('https') ? https : http;
|
||||||
|
var req = mod.request(url, { method: method, timeout: 5000, rejectUnauthorized: false }, function(res) {
|
||||||
|
var body = '';
|
||||||
|
res.on('data', function(c) { body += c; });
|
||||||
|
res.on('end', function() { cb(null, res.statusCode, body); });
|
||||||
|
});
|
||||||
|
req.on('error', function(e) { cb(e.message); });
|
||||||
|
req.on('timeout', function() { req.destroy(); cb('timeout'); });
|
||||||
|
req.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Эти тесты асинхронные — собираем результаты
|
||||||
|
var apiTests = [];
|
||||||
|
function addApiTest(name, method, url, check) {
|
||||||
|
apiTests.push({ name: name, method: method, url: url, check: check });
|
||||||
|
}
|
||||||
|
|
||||||
|
addApiTest('GET / (главная)', 'GET', 'https://contracts.kube5s.ru/', function(code, body) {
|
||||||
|
assert(code === 200, 'главная: HTTP 200 — got ' + code);
|
||||||
|
contains(body, '<!DOCTYPE html>', 'главная: HTML');
|
||||||
|
});
|
||||||
|
|
||||||
|
addApiTest('POST /api/cleanup', 'POST', 'https://contracts.kube5s.ru/api/cleanup', function(code, body) {
|
||||||
|
// cleanup может вернуть 200 или 500 (если БД недоступна) — оба норм для дымного теста
|
||||||
|
assert(code >= 200 && code < 600, 'cleanup: ответ получен — ' + code);
|
||||||
|
});
|
||||||
|
|
||||||
|
addApiTest('GET /static/state.js', 'GET', 'https://contracts.kube5s.ru/static/state.js', function(code, body) {
|
||||||
|
eq(code, 200, 'state.js: HTTP 200 — got ' + code);
|
||||||
|
contains(body, 'Центральное состояние', 'state.js: содержит описание');
|
||||||
|
});
|
||||||
|
|
||||||
|
addApiTest('GET /static/files.js', 'GET', 'https://contracts.kube5s.ru/static/files.js', function(code) {
|
||||||
|
eq(code, 200, 'files.js: HTTP 200 — got ' + code);
|
||||||
|
});
|
||||||
|
|
||||||
|
addApiTest('GET /static/groups.js', 'GET', 'https://contracts.kube5s.ru/static/groups.js', function(code) {
|
||||||
|
eq(code, 200, 'groups.js: HTTP 200 — got ' + code);
|
||||||
|
});
|
||||||
|
|
||||||
|
addApiTest('GET /static/compare.js', 'GET', 'https://contracts.kube5s.ru/static/compare.js', function(code) {
|
||||||
|
eq(code, 200, 'compare.js: HTTP 200 — got ' + code);
|
||||||
|
});
|
||||||
|
|
||||||
|
addApiTest('GET /static/app.js', 'GET', 'https://contracts.kube5s.ru/static/app.js', function(code) {
|
||||||
|
eq(code, 200, 'app.js: HTTP 200 — got ' + code);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Запускаем API тесты последовательно (чтобы не зафлудить)
|
||||||
|
var apiIdx = 0;
|
||||||
|
function runNextApiTest() {
|
||||||
|
if (apiIdx >= apiTests.length) {
|
||||||
|
// Все API тесты готовы — выводим итоги
|
||||||
|
printResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var t = apiTests[apiIdx];
|
||||||
|
apiTest(t.method, t.url, function(err, code, body) {
|
||||||
|
if (err) {
|
||||||
|
console.log(' FAIL: ' + t.name + ' — ' + err);
|
||||||
|
failed++;
|
||||||
|
} else {
|
||||||
|
t.check(code, body || '');
|
||||||
|
}
|
||||||
|
apiIdx++;
|
||||||
|
runNextApiTest();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function printResults() {
|
||||||
console.log('\n========================================');
|
console.log('\n========================================');
|
||||||
console.log('PASS: ' + passed);
|
console.log('PASS: ' + passed);
|
||||||
console.log('FAIL: ' + failed);
|
console.log('FAIL: ' + failed);
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
|
if (failed > 0) process.exit(1);
|
||||||
if (failed > 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Начинаем с API тестов после синхронных
|
||||||
|
console.log(' (асинхронные — ждём...)');
|
||||||
|
runNextApiTest();
|
||||||
|
|||||||
Reference in New Issue
Block a user