arm-core/web/src/widgets/DocumentReview/statementDraft.test.ts
Andrey Epifancev 1a35829074 fix(web): синхронизация встроенного фронта с arm-web@test (4a02512) (#133)
Перенесён диапазон arm-web 0f2bbc8..4a02512 (23 коммитов, 51 файлов набора
синхронизации) скриптом docs/scripts/sync-web.sh. После переноса web/ и
arm-web@test идентичны по набору. Коммиты arm-web:
- 4fb477d Подпись пустой цели «цель не заполнена», тесты null/null (ревью I-1, M-1, M-2, M-4)
- cb36ec8 Нарушения проверок: expected/actual и индекс колонки могут быть null (#45)
- b4b8c04 openspec(statements-v2): решения по итогам прогона в браузере (T11.D-1…D-5, #43)
- aa3054e docs(arm-web): экран формы v5.19 — README и карта модулей (#43)
- 88a4920 fix(arm-web): нераспознанный текст суммы, пустые подстроки без расшифровок, свёрнутый список предупреждений (#43)
- a3199d6 fix(arm-web): вёрстка экрана проверки формы — ширины колонок, значки в ячейке, шапка, ссылка кнопкой (#43)
- 12fcea7 fix(statement): защитные значения по умолчанию для ядра без v5.19 (M-2, #43)
- 4783593 fix(statement): ссылка на строки без кода всегда видна, сортировка по row_index, Alert для ошибки скрытого поля кода, постоянный aria-label (T9.1, T9.2, T9.4, T7.1, T8.1, T3.3, M-3, #43)
- d965fdc fix(statement): округление уверенности в подсказках шапки, пустые тексты в подсказках, комментарий шапки (T5.1, T5.2, M-1, T3.1, #43)
- 6556e3d fix(statement): не отправлять поля шапки, равные текущим (I-2, #43)
- 8d2deb9 docs(openspec): решения без обсуждения из исполнения и имя кнопки «Подтвердить данные» (#43)
- fc5cc16 fix(files): клики внутри popover не открывают строку, ключи и aria-label (#43)
- fdf9358 feat(files): предупреждения файла структурами и предупреждения документа в панели файлов и у ОСВ (#43)
- 5b11f2d feat(statement): блокировка подтверждения при строках без кода, LINES_UNRESOLVED и DUPLICATE_FORM (#43)
- a779d2a feat(statement): подтверждение ячеек, дата колонки, наименование организации и подтверждение даты (#43)
- d416422 feat(statement): правка строк — код с подсказкой из справочника, меню строки, LINE_CODE_TAKEN (#43)
- ef4d325 refactor(statement): один черновик правок и сборка единственного PATCH (#43)
- 8ab116f feat(statement): шапка — организация, поля на проверку и подсказки распознанного текста (#43)
- 4ae8643 feat(statement): предупреждения и нарушения у строк, ячеек, колонок и в списке документа (#43)
- 34d523a feat(statement): вид строк по статусам, подсветка по needs_review, переключатель расшифровок (#43)
- c2f33e7 feat(statement): раскладка строк без кода и расшифровок на своих местах (#43)
- e4ee7f0 fix(fixtures): коды соотношения, column_index, порядок edition_lines (#43)
- f83b035 feat(types): API формы v5.19 — статусы строк, needs_review, issues, предупреждения файла (#43)

Co-Authored-By: sync-web.sh <noreply@aepif.ru>
2026-10-18 22:56:49 +04:00

161 lines
8.6 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 } from '../../types/api';
import statementV2Json from './__fixtures__/statement-v2.json';
import { type StatementDraft, buildStatementPatch, emptyDraft, isDraftEmpty } from './statementDraft';
import { buildStatementGrid, cellKey } from './statementGrid';
const statement = statementV2Json as StatementOut;
const { rows } = buildStatementGrid(statement);
const ctx = { rows, header: statement.header };
const draftOf = (part: Partial<StatementDraft>): StatementDraft => ({ ...emptyDraft(), ...part });
const build = (part: Partial<StatementDraft>) => buildStatementPatch(draftOf(part), ctx);
const bodyOf = (part: Partial<StatementDraft>) => {
const result = build(part);
if (!result.ok) throw new Error(`ожидали тело, получили ошибки: ${JSON.stringify(result.errors)}`);
return result.body;
};
const errorsOf = (part: Partial<StatementDraft>) => {
const result = build(part);
if (result.ok) throw new Error('ожидали ошибки');
return result.errors;
};
describe('суммы как раньше', () => {
it('строка из БД — line_id, шаблонная — line_code', () => {
expect(bodyOf({ amounts: { [cellKey('l-1210', 'c1')]: '120' } })?.values).toEqual([
{ line_id: 'l-1210', column_id: 'c1', amount: '120' },
]);
expect(bodyOf({ amounts: { [cellKey('code-1250', 'c1')]: '5' } })?.values).toEqual([
{ line_code: '1250', column_id: 'c1', amount: '5' },
]);
});
it('пустой текст — очистка суммы (null)', () => {
expect(bodyOf({ amounts: { [cellKey('l-1210', 'c1')]: '' } })?.values).toEqual([{ line_id: 'l-1210', column_id: 'c1', amount: null }]);
});
it('неверный формат — ошибка amount с ключом ячейки', () => {
const key = cellKey('l-1210', 'c1');
expect(errorsOf({ amounts: { [key]: '12,3,4' } })).toEqual([expect.objectContaining({ kind: 'amount', key })]);
});
it('неизвестная строка — ошибка, а не тихий пропуск', () => {
expect(errorsOf({ amounts: { [cellKey('ghost', 'c1')]: '1' } })[0].kind).toBe('amount');
});
});
describe('подтверждение ячейки', () => {
it('confirms → элемент confirmed', () => {
expect(bodyOf({ confirms: { [cellKey('l-u7', 'c1')]: true } })?.values).toEqual([
{ line_id: 'l-u7', column_id: 'c1', confirmed: true },
]);
});
it('сумма той же ячейки отменяет подтверждение (в элементе ровно одно из amount / confirmed)', () => {
const key = cellKey('l-u7', 'c1');
expect(bodyOf({ amounts: { [key]: '7' }, confirms: { [key]: true } })?.values).toEqual([
{ line_id: 'l-u7', column_id: 'c1', amount: '7' },
]);
});
});
describe('подтверждение ячейки без id значения', () => {
it('пропускается молча (P6-6)', () => {
const row = rows.find((r) => r.key === 'l-u7');
if (!row?.cells.c1) throw new Error('нет ячейки в фикстуре');
const noId = { ...row, cells: { ...row.cells, c1: { ...row.cells.c1, id: null } } };
const result = buildStatementPatch(draftOf({ confirms: { [cellKey('l-u7', 'c1')]: true } }), {
rows: rows.map((r) => (r === row ? noId : r)),
header: statement.header,
});
expect(result).toEqual({ ok: true, body: null });
});
});
describe('действия строки', () => {
it('код, расшифровка, подтверждение кода', () => {
expect(bodyOf({ lines: { 'l-u7': { kind: 'code', code: '1250' } } })?.lines).toEqual([{ line_id: 'l-u7', line_code: '1250' }]);
expect(bodyOf({ lines: { 'l-m8': { kind: 'detail' } } })?.lines).toEqual([{ line_id: 'l-m8', line_status: 'detail' }]);
expect(bodyOf({ lines: { 'l-1230': { kind: 'confirm', confirmed: true } } })?.lines).toEqual([{ line_id: 'l-1230', confirmed: true }]);
});
it('пустой код и код, равный текущему, — не действие', () => {
expect(bodyOf({ lines: { 'l-u7': { kind: 'code', code: '' } } })).toBeNull();
expect(bodyOf({ lines: { 'l-1230': { kind: 'code', code: '1230' } } })).toBeNull();
});
it('код — 4 или 5 цифр', () => {
for (const code of ['12a4', '123']) {
expect(errorsOf({ lines: { 'l-u7': { kind: 'code', code } } })).toEqual([
{ kind: 'code', key: 'l-u7', message: 'Код — 4 или 5 цифр' },
]);
}
expect(bodyOf({ lines: { 'l-u7': { kind: 'code', code: '11501' } } })?.lines).toEqual([{ line_id: 'l-u7', line_code: '11501' }]);
});
it('действие строки и сумма той же строки — оба в теле', () => {
const body = bodyOf({ lines: { 'l-m8': { kind: 'detail' } }, amounts: { [cellKey('l-m8', 'c1')]: '8' } });
expect(body?.lines).toEqual([{ line_id: 'l-m8', line_status: 'detail' }]);
expect(body?.values).toEqual([{ line_id: 'l-m8', column_id: 'c1', amount: '8' }]);
});
it('сумма в шаблонную строку 1250 и назначение 1250 другой строке — конфликт', () => {
expect(errorsOf({ amounts: { [cellKey('code-1250', 'c1')]: '5' }, lines: { 'l-u7': { kind: 'code', code: '1250' } } })).toEqual([
{
kind: 'conflict',
key: 'l-u7',
message: 'Код 1250 назначается строке, а в шаблонную строку 1250 введены суммы — сохраните по очереди',
},
]);
});
});
describe('колонки', () => {
it('дата колонки ГГГГ-ММ-ДД', () => {
expect(bodyOf({ columns: { c2: '2024-12-31' } })?.columns).toEqual([{ column_id: 'c2', period_date: '2024-12-31' }]);
});
it('другой формат и несуществующая дата — ошибка date; пусто — не отправляется', () => {
for (const text of ['31.12.2024', '2024-02-30']) {
expect(errorsOf({ columns: { c2: text } })).toEqual([{ kind: 'date', key: 'c2', message: 'Дата в формате ГГГГ-ММ-ДД' }]);
}
expect(bodyOf({ columns: { c2: '' } })).toBeNull();
});
});
describe('шапка', () => {
it('organization_name обрезается', () => {
expect(bodyOf({ header: { organization_name: ' ООО Ромашка-2 ' } })?.header).toEqual({ organization_name: 'ООО Ромашка-2' });
});
it('значение, равное текущему (после обрезки), не отправляется', () => {
expect(bodyOf({ header: { organization_name: ' ООО Ромашка ' } })).toBeNull();
expect(bodyOf({ header: { inn: '7700000000', units: ' тыс. руб. ' } })).toBeNull();
});
it('введено и стёрто — тела нет', () => {
expect(bodyOf({ header: { inn: ' ', organization_name: '' } })).toBeNull();
});
it('дата из needs_review_fields с тем же значением — подтверждение, уходит', () => {
expect(bodyOf({ header: { report_date: '2025-09-30' } })?.header).toEqual({ report_date: '2025-09-30' });
const noReview = { rows, header: { ...statement.header, needs_review_fields: [] } };
const res = buildStatementPatch(draftOf({ header: { report_date: '2025-09-30' } }), noReview);
expect(res).toEqual({ ok: true, body: null });
});
it('длиннее 1000 символов — ошибка header', () => {
expect(errorsOf({ header: { organization_name: 'я'.repeat(1001) } })[0]).toMatchObject({ kind: 'header', key: 'organization_name' });
expect(bodyOf({ header: { organization_name: 'я'.repeat(1000) } })).not.toBeNull();
});
it('подтверждение даты отчёта отправляет текущее значение', () => {
expect(bodyOf({ header: { confirm_report_date: true } })?.header).toEqual({ report_date: '2025-09-30' });
expect(bodyOf({ header: { confirm_report_date: true, report_date: '2025-06-30' } })?.header).toEqual({ report_date: '2025-06-30' });
});
it('пустое поле не отправляется', () => {
expect(bodyOf({ header: { inn: '' } })).toBeNull();
expect(bodyOf({ header: { inn: ' 7700000001 ', units: '', report_date: '2025-09-30' } })?.header).toEqual({
inn: '7700000001',
report_date: '2025-09-30',
});
});
});
describe('пустой черновик', () => {
it('isDraftEmpty и body null', () => {
expect(isDraftEmpty(emptyDraft())).toBe(true);
expect(buildStatementPatch(emptyDraft(), ctx)).toEqual({ ok: true, body: null });
});
it('любая часть делает черновик непустым', () => {
expect(isDraftEmpty(draftOf({ header: { inn: '1' } }))).toBe(false);
expect(isDraftEmpty(draftOf({ lines: { 'l-m8': { kind: 'detail' } } }))).toBe(false);
});
});