149 lines
6.6 KiB
JavaScript
149 lines
6.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import http from 'node:http';
|
||
import test from 'node:test';
|
||
import {
|
||
SYNTHETIC_ROWS,
|
||
compareCase,
|
||
deriveCases,
|
||
runCheck,
|
||
emitNumberVectors,
|
||
emitScreenVectors,
|
||
equals,
|
||
matchesSearch,
|
||
screenFacets,
|
||
screenFound,
|
||
screenRows,
|
||
} from './svod2_search_oracle.mjs';
|
||
|
||
const mk = (id, type, dir, org) => ({ data: { line_id: id, header: { smeta_type: type, smeta_direction: dir, org_name: org } } });
|
||
const rows2 = [mk(1, 'КВ', 'Развитие', 'А'), mk(2, 'АХР', 'Поддержка', 'Б')];
|
||
const rows3 = [mk(1, 'КВ', 'Р', 'Филиал Ёлки'), mk(2, 'КВ', 'Р', 'Филиал Ёлки'), mk(3, 'КВ', 'Р', null)];
|
||
|
||
test('перекрытое поле contract_summary не ищется', () => {
|
||
const row = { data: { header: { name: 'x' }, contract_summary: { counterparty: 'ООО Ромашка' }, contract_detail: { counterparty: null } } };
|
||
assert.equal(matchesSearch(row, 'ромаш'), false);
|
||
});
|
||
test('число ищется в записи JS', () => {
|
||
const row = { data: { header: {}, contract_summary: {}, contract_detail: JSON.parse('{"ceiling_amount": 1234.50}') } };
|
||
assert.equal(matchesSearch(row, '1234.5'), true);
|
||
assert.equal(matchesSearch(row, '1234.50'), false);
|
||
});
|
||
test('screenRows: trim + lower, NBSP по краям', () => {
|
||
const rows = [{ data: { line_id: 1, header: { name: 'Аренда помещения' } } }];
|
||
assert.deepEqual(screenRows(rows, { search: ' АРЕНДА ' }).map((r) => r.data.line_id), [1]);
|
||
assert.deepEqual(screenRows(rows, { search: ' аренда ' }).map((r) => r.data.line_id), [1]);
|
||
});
|
||
test('equals: регистр и пробелы по краям, null не проходит', () => {
|
||
const r = (v) => ({ getValue: () => v });
|
||
assert.equal(equals(r('КВ'), 'smeta_type', 'кв '), true);
|
||
assert.equal(equals(r(null), 'smeta_type', 'кв'), false);
|
||
});
|
||
test('screenFacets: свой фильтр не применяется, чужие — да', () => {
|
||
const f = screenFacets(rows2, { filters: { smeta_type: 'КВ' } });
|
||
assert.deepEqual(f.smeta_type, [['АХР', 1], ['КВ', 1]]);
|
||
assert.deepEqual(f.smeta_direction, [['Развитие', 1]]);
|
||
});
|
||
test('screenFacets: одинаковые названия — один вариант, null не выдаётся', () => {
|
||
assert.deepEqual(screenFacets(rows3).org_name, [['Филиал Ёлки', 2]]);
|
||
});
|
||
test('screenFound считает только поиск', () => {
|
||
assert.deepEqual(screenFound(rows2, ''), { found: 2, total: 2 });
|
||
});
|
||
test('векторы чисел', () => {
|
||
const v = Object.fromEntries(emitNumberVectors().map(({ input, js }) => [input, js]));
|
||
assert.equal(v['1e21'], '1e+21');
|
||
assert.equal(v['-0'], '0');
|
||
assert.equal(v['1e-7'], '1e-7');
|
||
assert.equal(v['0.000001'], '0.000001');
|
||
assert.equal(v['100.00'], '100');
|
||
assert.equal(v['9007199254740993'], '9007199254740992');
|
||
});
|
||
test('векторы экрана: не меньше 12 случаев', () => {
|
||
const s = emitScreenVectors();
|
||
assert.ok(s.cases.length >= 12);
|
||
assert.ok(s.rows.length >= 10 && s.rows.length <= 15);
|
||
});
|
||
|
||
test('deriveCases: не меньше 8 случаев и есть дробное число', () => {
|
||
const cases = deriveCases(SYNTHETIC_ROWS);
|
||
assert.ok(cases.length >= 8);
|
||
for (const required of ['аренда', 'АРЕНДА', '1', ',']) {
|
||
assert.ok(cases.some((c) => c.search === required && Object.keys(c.filters).length === 0), required);
|
||
}
|
||
assert.ok(cases.some((c) => /\d\.\d|\de-\d/.test(c.search)));
|
||
});
|
||
|
||
const expectedFor = (rows, { search, filters }) => ({
|
||
line_ids: screenRows(rows, { search, filters }).map((r) => r.data.line_id),
|
||
total: screenRows(rows, { search, filters }).length,
|
||
found: screenFound(rows, search).found,
|
||
facets: screenFacets(rows, { search, filters }),
|
||
});
|
||
|
||
test('compareCase: совпадение даёт пустой массив, различия находятся', () => {
|
||
const base = expectedFor(SYNTHETIC_ROWS, { search: 'ё', filters: {} });
|
||
assert.deepEqual(compareCase(base, structuredClone(base)), []);
|
||
const swapped = structuredClone(base);
|
||
swapped.line_ids.reverse();
|
||
assert.ok(compareCase(base, swapped).length > 0);
|
||
const counted = structuredClone(base);
|
||
counted.facets.smeta_type[0][1] += 1;
|
||
assert.ok(compareCase(base, counted).length > 0);
|
||
});
|
||
|
||
const startStub = async ({ corrupt = false } = {}) => {
|
||
const server = http.createServer((req, res) => {
|
||
const url = new URL(req.url, 'http://x');
|
||
const send = (body, status = 200) => {
|
||
res.writeHead(status, { 'content-type': 'application/json' });
|
||
res.end(JSON.stringify(body));
|
||
};
|
||
if (url.pathname === '/api/v1/auth/login') return send({ access_token: 't' });
|
||
const m = url.pathname.match(/^\/api\/v1\/svod\/2\/(GO|RF)\/(\d+)(\/facets)?$/);
|
||
if (!m) return send({}, 404);
|
||
const q = url.searchParams;
|
||
const opts = {
|
||
search: q.get('search') ?? '',
|
||
filters: Object.fromEntries(['smeta_type', 'smeta_direction', 'org_name'].filter((k) => q.get(k)).map((k) => [k, q.get(k)])),
|
||
};
|
||
const hasNew = ['view', 'search', 'smeta_type', 'smeta_direction', 'org_name'].some((k) => q.has(k));
|
||
if (!hasNew) return send({ result: SYNTHETIC_ROWS.map((r) => ({ row_type: 'row', depth: 0, sort_order: null, data: r.data })) });
|
||
if (m[3]) {
|
||
const f = screenFacets(SYNTHETIC_ROWS, opts);
|
||
const out = { total: f.total, search_total: f.search_total };
|
||
for (const id of ['smeta_type', 'smeta_direction', 'org_name']) out[id] = f[id].map(([value, count]) => ({ value, count }));
|
||
return send({ result: out });
|
||
}
|
||
const all = screenRows(SYNTHETIC_ROWS, opts);
|
||
const offset = Number(q.get('offset') ?? 0);
|
||
const limit = Number(q.get('limit') ?? all.length);
|
||
let page = all.slice(offset, offset + limit);
|
||
if (corrupt && offset === 0) page = page.slice().reverse();
|
||
return send({ count: page.length, total: all.length, result: page.map((r) => ({ row_type: 'row', depth: 0, sort_order: null, data: r.data })) });
|
||
});
|
||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||
return { server, api: `http://127.0.0.1:${server.address().port}` };
|
||
};
|
||
|
||
test('runCheck на заглушке API: ok true', async () => {
|
||
const { server, api } = await startStub();
|
||
try {
|
||
const r = await runCheck({ api, username: 'u', password: 'p', year: 2026, pageSize: 3 });
|
||
assert.deepEqual(r.report, []);
|
||
assert.equal(r.ok, true);
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|
||
|
||
test('runCheck: испорченная страница даёт ok false', async () => {
|
||
const { server, api } = await startStub({ corrupt: true });
|
||
try {
|
||
const r = await runCheck({ api, username: 'u', password: 'p', year: 2026, pageSize: 3 });
|
||
assert.equal(r.ok, false);
|
||
assert.ok(r.report.length > 0);
|
||
} finally {
|
||
server.close();
|
||
}
|
||
});
|