arm-web/src/widgets/DocumentReview/statementGrid.test.ts
Andrey Epifancev f83b03543e feat(types): API формы v5.19 — статусы строк, needs_review, issues, предупреждения файла (#43)
- types/api.ts: LineStatus, Severity, IssueKind, PeriodKind, FileWarningOut, IssueOut, EditionLineOut,
  IssueCountsOut, CellTarget, LinePatch, ColumnPatch; новые поля StatementHeaderOut, ColumnOut, ValueOut,
  LineOut, FailedCheckOut, StatementOut (form_variant, issues, edition_lines), DocumentBrief (issue_counts),
  DocumentOut (issues), JobBrief.warnings — FileWarningOut[].
- statementGrid.ts: GridRow получает статус строки, номер строки, код как распознан, флаги needs_review /
  is_confirmed / codeModified; cellFlags: внимание только по needs_review, новый флаг confirmed.
- labels.ts: lineStatusLabel, severityLabel.
- фикстура __fixtures__/statement-v2.json (БАЛАНС 2025 Q3, NEEDS_CHECK): 16 строк × 2 колонки, у каждой пары ValueOut.
- тесты: переносы полей v2 и покрытие пар строка × колонка, cellFlags по needs_review, подписи; хелперы
  row() / v() / gridRow / doc() дополнены значениями по умолчанию.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 13:15:29 +04:00

197 lines
8.3 KiB
TypeScript
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.

import { describe, expect, it } from 'vitest';
import type { StatementOut, ValueOut } from '../../types/api';
import statementV2Json from './__fixtures__/statement-v2.json';
import type { GridRow } from './statementGrid';
import { applyDraftInput, buildStatementGrid, cellFlags, cellKey, draftToPatch, emptySublineKeys } from './statementGrid';
const v = (id: string | null, column_id: string, column_index: number, extra: Partial<ValueOut> = {}): ValueOut => ({
id,
column_id,
column_index,
amount: '1.00',
string_value: null,
text: null,
confidence: '100',
is_recognized: true,
is_modified: false,
original_amount: null,
needs_review: false,
is_confirmed: false,
...extra,
});
// Фикстура v5.19 (тип StatementOut): приведение нужно, т.к. импорт JSON расширяет строковые литералы до string
const statementV2 = statementV2Json as StatementOut;
const statement = {
columns: [
{ id: 'c2', column_index: 1, label: '31.12.2023', period_date: null, is_current: false },
{ id: 'c1', column_index: 0, label: '30.09.2024', period_date: null, is_current: true },
],
lines: [{ id: 'l1', origin: 'source', line_code: '1600', line_name: 'Баланс', values: [v('v2', 'c2', 1), v('v1', 'c1', 0)] }],
} as unknown as StatementOut;
describe('buildStatementGrid', () => {
it('колонки по column_index, ячейки по column_id', () => {
const grid = buildStatementGrid(statement);
expect(grid.columns.map((c) => c.id)).toEqual(['c1', 'c2']);
expect(grid.rows[0]).toMatchObject({ key: 'l1', lineId: 'l1', code: '1600', name: 'Баланс', origin: 'source' });
expect(grid.rows[0].cells.c1?.id).toBe('v1');
expect(grid.rows[0].cells.c2?.id).toBe('v2');
});
it('буквально переносит поля v2 из фикстуры', () => {
const { rows } = buildStatementGrid(statementV2);
const byKey = new Map(rows.map((row) => [row.key, row]));
expect(byKey.get('l-u7')).toMatchObject({
lineStatus: 'unreadable',
rowIndex: 7,
codeText: 'ИЧ,250',
code: null,
key: 'l-u7',
});
expect(byKey.get('l-d2')?.parentRowIndex).toBe(1);
expect(byKey.get('code-1250')).toMatchObject({
key: 'code-1250',
rowIndex: null,
referenceName: 'Денежные средства и денежные эквиваленты',
});
expect(byKey.get('l-1230')).toMatchObject({ needsReview: true, codeConfidence: '50' });
});
it('каждая пара «строка × колонка» фикстуры имеет ValueOut', () => {
const { columns, rows } = buildStatementGrid(statementV2);
for (const row of rows) {
for (const column of columns) {
expect(row.cells[column.id], `${row.key} × ${column.id}`).toBeDefined();
}
}
});
it('достроенная строка: ключ по коду, lineId = null', () => {
const withTemplate = {
...statement,
lines: [
...statement.lines,
{
id: null,
origin: 'template',
line_code: '1700',
line_name: 'БАЛАНС',
values: [v(null, 'c1', 0, { amount: null, confidence: null, is_recognized: null })],
},
],
} as unknown as StatementOut;
const row = buildStatementGrid(withTemplate).rows[1];
expect(row).toMatchObject({ key: 'code-1700', lineId: null, code: '1700', origin: 'template' });
});
});
describe('cellFlags — внимание только по needs_review', () => {
it('низкая уверенность и is_recognized=false без needs_review — не внимание', () => {
expect(cellFlags(v('a', 'c', 0, { confidence: '50', is_recognized: false, needs_review: false })).attention).toBe(false);
});
it('needs_review=true — внимание, даже при уверенности 100', () => {
expect(cellFlags(v('a', 'c', 0, { confidence: '100', needs_review: true })).attention).toBe(true);
});
it('is_confirmed — подтверждена, уверенность не меняется', () => {
expect(cellFlags(v('a', 'c', 0, { is_confirmed: true })).confirmed).toBe(true);
});
it('undefined — все флаги false', () => {
expect(cellFlags(undefined)).toEqual({ attention: false, modified: false, confirmed: false });
});
it('пустая достроенная ячейка не требует внимания', () => {
expect(cellFlags(v(null, 'c', 0, { amount: null, confidence: null, is_recognized: null }))).toEqual({
attention: false,
modified: false,
confirmed: false,
});
});
it('исправлено', () => {
expect(cellFlags(v('a', 'c', 0, { is_modified: true })).modified).toBe(true);
});
});
describe('draftToPatch', () => {
const rows = buildStatementGrid({
...statement,
lines: [...statement.lines, { id: null, origin: 'template', line_code: '1700', line_name: 'БАЛАНС', values: [] }],
} as unknown as StatementOut).rows;
it('строка из БД — line_id, достроенная — line_code', () => {
expect(draftToPatch({ [cellKey('l1', 'c1')]: '10.5', [cellKey('code-1700', 'c1')]: null }, rows)).toEqual([
{ line_id: 'l1', column_id: 'c1', amount: '10.5' },
{ line_code: '1700', column_id: 'c1', amount: null },
]);
});
it('неизвестная строка — ошибка, а не тихий пропуск', () => {
expect(() => draftToPatch({ [cellKey('ghost', 'c1')]: '1' }, rows)).toThrow();
});
});
describe('applyDraftInput', () => {
const parse = (s: string): { ok: true; value: string | null } | { ok: false } => {
const n = s.replace(/\s/g, '').replace(',', '.');
if (n === '') {
return { ok: true, value: null };
}
return /^-?\d+(\.\d+)?$/.test(n) ? { ok: true, value: n } : { ok: false };
};
it('изменение попадает в черновик', () => {
expect(applyDraftInput({}, 'k', '5', '1.00', parse)).toEqual({ k: '5' });
});
it('возврат к исходному убирает ключ (с учётом нулей дроби)', () => {
expect(applyDraftInput({ k: '5' }, 'k', '1', '1.00', parse)).toEqual({});
expect(applyDraftInput({ k: '5' }, 'k', '1,0', '1.00', parse)).toEqual({});
});
it('пустое при пустом исходном — не изменение', () => {
expect(applyDraftInput({ k: '5' }, 'k', '', null, parse)).toEqual({});
});
it('неверный формат остаётся в черновике', () => {
expect(applyDraftInput({}, 'k', '12,3,4', '1.00', parse)).toEqual({ k: '12,3,4' });
});
});
describe('parent_code и пустые строки детализации', () => {
it('buildStatementGrid переносит parent_code', () => {
const withChild = {
...statement,
lines: [{ id: 'l2', origin: 'source', line_code: '11101', line_name: 'п', parent_code: '1110', values: [] }, ...statement.lines],
} as unknown as StatementOut;
const grid = buildStatementGrid(withChild);
expect(grid.rows[0].parentCode).toBe('1110');
expect(grid.rows[1].parentCode).toBeNull(); // поля нет в ответе (старый сервер) — не подстрока
});
const cell = (extra: Partial<ValueOut>) => v('v', 'c1', 0, { amount: null, ...extra });
const gridRow = (key: string, parentCode: string | null, cells: GridRow['cells']): GridRow => ({
key,
lineId: key,
code: key,
name: null,
origin: 'source',
parentCode,
cells,
referenceName: null,
lineStatus: 'ok',
rowIndex: null,
parentRowIndex: null,
codeText: key,
codeConfidence: null,
needsReview: false,
isConfirmed: false,
codeModified: false,
});
it('пустая подстрока: все ячейки без суммы и без строки; родитель и нулевая сумма — не пустые', () => {
const rows = [
gridRow('11101', '1110', { c1: cell({}), c2: cell({}) }),
gridRow('11102', '1110', {}),
gridRow('11103', '1110', { c1: cell({ amount: '0.00' }) }),
gridRow('11104', '1110', { c1: cell({ string_value: 'нет' }) }),
gridRow('11105', '1110', { c1: cell({ amount: '5.00' }) }),
gridRow('1110', null, { c1: cell({}) }),
];
expect([...emptySublineKeys(rows, {})]).toEqual(['11101', '11102']);
});
it('подстрока с правкой в черновике не считается пустой', () => {
const rows = [gridRow('11101', '1110', { c1: cell({}) }), gridRow('11102', '1110', { c1: cell({}) })];
expect([...emptySublineKeys(rows, { '11101:c1': '7' })]).toEqual(['11102']);
});
});