225 lines
9.6 KiB
TypeScript
225 lines
9.6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
||
import type { CounterPairOut } from '../types/api';
|
||
import type { ConsStatus, LockOut } from '../types/consolidations/common_types';
|
||
import {
|
||
CONS_STATUS_LABEL,
|
||
FINISHED_STATUSES,
|
||
MANUAL_TRANSITIONS,
|
||
canChangeStatus,
|
||
canSubmitStatusChange,
|
||
canTakeInWork,
|
||
consStatusDescription,
|
||
counterPairsKey,
|
||
counterPairsWarning,
|
||
endSentence,
|
||
isLockedBy,
|
||
isValidReason,
|
||
manualTransitions,
|
||
parseInWorkOnly,
|
||
rememberCounterPairsShown,
|
||
reportTabNavigation,
|
||
shouldWarnCounterPairs,
|
||
versionLabel,
|
||
} from './consolidations';
|
||
|
||
const lock = (userId: string): LockOut => ({
|
||
locked_by: userId,
|
||
locked_by_name: 'Соколов А. В.',
|
||
locked_at: '2026-10-23T10:00:00Z',
|
||
taken_over_from: null,
|
||
takeover_reason: null,
|
||
});
|
||
|
||
const ALL: ConsStatus[] = Object.keys(CONS_STATUS_LABEL) as ConsStatus[];
|
||
|
||
describe('versionLabel', () => {
|
||
it('первая версия — пусто, последующие — номер', () => {
|
||
expect(versionLabel(1)).toBe('');
|
||
expect(versionLabel(2)).toBe('2');
|
||
expect(versionLabel(10)).toBe('10');
|
||
});
|
||
});
|
||
|
||
describe('parseInWorkOnly', () => {
|
||
it('по умолчанию выключен, включает только явное true', () => {
|
||
expect(parseInWorkOnly(null)).toBe(false);
|
||
expect(parseInWorkOnly('')).toBe(false);
|
||
expect(parseInWorkOnly('false')).toBe(false);
|
||
expect(parseInWorkOnly('true')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('подписи и пояснения статусов', () => {
|
||
it('подписи из спеки', () => {
|
||
expect(CONS_STATUS_LABEL.DATA_CORRECTION).toBe('Корректировка данных');
|
||
expect(CONS_STATUS_LABEL.CANCELLED).toBe('Отменена');
|
||
});
|
||
it('пояснение не повторяет подпись', () => {
|
||
for (const s of ALL) {
|
||
expect(consStatusDescription(s)).not.toBe(CONS_STATUS_LABEL[s]);
|
||
}
|
||
});
|
||
it('пояснения статусов первой очереди и макета заданы', () => {
|
||
for (const s of ['INITIATED', 'DOCUMENTS_PROCESSING', 'DATA_CORRECTION', 'CANCELLED', 'CONSOLIDATION_RF', 'VERIFIED'] as const) {
|
||
expect(consStatusDescription(s)).not.toBe('');
|
||
}
|
||
expect(consStatusDescription('CONSOLIDATION_RF')).toBe('Автоматическая консолидация, ручные проверки и сравнение КФО клиента и банка');
|
||
expect(consStatusDescription('VERIFIED')).toBe('Отчет верифицирован ГО, КА ГО внес в систему номер и дату решения УОБ');
|
||
});
|
||
});
|
||
|
||
describe('ручные переходы (зеркало MANUAL_TRANSITIONS бэка)', () => {
|
||
it('совпадают с api/src/services/consolidation_status.py', () => {
|
||
expect(MANUAL_TRANSITIONS).toEqual({
|
||
INITIATED: ['CANCELLED'],
|
||
DOCUMENTS_PROCESSING: ['CANCELLED', 'INITIATED'],
|
||
DATA_CORRECTION: ['CANCELLED', 'INITIATED'],
|
||
});
|
||
});
|
||
it('из остальных статусов переходов нет', () => {
|
||
for (const s of ['VERIFIED', 'CANCELLED', 'CONSOLIDATION_RF', 'VERIFICATION_RF', 'CONSOLIDATION_GO', 'VERIFICATION_GO'] as const) {
|
||
expect(manualTransitions(s)).toEqual([]);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('блокировка и «Изменить статус»', () => {
|
||
it('своя блокировка', () => {
|
||
expect(isLockedBy(lock('me'), 'me')).toBe(true);
|
||
expect(isLockedBy(lock('other'), 'me')).toBe(false);
|
||
expect(isLockedBy(null, 'me')).toBe(false);
|
||
expect(isLockedBy(undefined, 'me')).toBe(false);
|
||
});
|
||
it('кнопка: своя / чужая / нет блокировки × есть / нет переходов', () => {
|
||
expect(canChangeStatus('DATA_CORRECTION', lock('me'), 'me')).toBe(true);
|
||
expect(canChangeStatus('DATA_CORRECTION', lock('other'), 'me')).toBe(false);
|
||
expect(canChangeStatus('DATA_CORRECTION', null, 'me')).toBe(false);
|
||
expect(canChangeStatus('VERIFIED', lock('me'), 'me')).toBe(false);
|
||
expect(canChangeStatus('VERIFIED', lock('other'), 'me')).toBe(false);
|
||
expect(canChangeStatus('VERIFIED', null, 'me')).toBe(false);
|
||
});
|
||
it('«Взять в работу» недоступна только у отменённой версии', () => {
|
||
expect(canTakeInWork('CANCELLED')).toBe(false);
|
||
expect(canTakeInWork('INITIATED')).toBe(true);
|
||
expect(canTakeInWork('VERIFIED')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('причина и комментарий', () => {
|
||
it('пустая и из пробелов — неверна', () => {
|
||
expect(isValidReason('')).toBe(false);
|
||
expect(isValidReason(' ')).toBe(false);
|
||
expect(isValidReason(undefined)).toBe(false);
|
||
expect(isValidReason(null)).toBe(false);
|
||
});
|
||
it('ровно 1000 символов можно, 1001 — нет', () => {
|
||
expect(isValidReason('a'.repeat(1000))).toBe(true);
|
||
expect(isValidReason('a'.repeat(1001))).toBe(false);
|
||
expect(isValidReason(` ${'a'.repeat(1000)} `)).toBe(true);
|
||
});
|
||
it('модалка: нужен выбранный этап и непустой комментарий', () => {
|
||
expect(canSubmitStatusChange(undefined, 'Дубль')).toBe(false);
|
||
expect(canSubmitStatusChange('CANCELLED', undefined)).toBe(false);
|
||
expect(canSubmitStatusChange('CANCELLED', ' ')).toBe(false);
|
||
expect(canSubmitStatusChange('CANCELLED', 'Дубль')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('endSentence', () => {
|
||
it('не ставит вторую точку после инициалов', () => {
|
||
expect(endSentence('Соколов А. В.')).toBe('Соколов А. В.');
|
||
expect(endSentence('Dev: Аналитик ГО')).toBe('Dev: Аналитик ГО.');
|
||
});
|
||
});
|
||
|
||
describe('вкладки отчёта (AURORA-1851)', () => {
|
||
it('смена вкладки заменяет запись истории: «Назад» ведёт со страницы отчёта, а не по вкладкам', () => {
|
||
expect(reportTabNavigation('/groups/g1/versions/v1/osv', 'osv', 'chess')).toEqual({
|
||
to: '/groups/g1/versions/v1/chess',
|
||
options: { replace: true },
|
||
});
|
||
});
|
||
});
|
||
|
||
const pair = (holder: string, missing: string): CounterPairOut => ({
|
||
holder_id: holder,
|
||
holder_name: `H${holder}`,
|
||
missing_id: missing,
|
||
missing_name: `M${missing}`,
|
||
holder_rows: 1,
|
||
missing_unmapped_rows: 0,
|
||
missing_rows_total: 0,
|
||
});
|
||
|
||
describe('counterPairsKey', () => {
|
||
it('не зависит от порядка', () => {
|
||
expect(counterPairsKey([pair('a', 'b'), pair('c', 'd')])).toBe(counterPairsKey([pair('c', 'd'), pair('a', 'b')]));
|
||
expect(counterPairsKey([pair('c', 'd'), pair('a', 'b')])).toBe('a>b,c>d');
|
||
});
|
||
});
|
||
|
||
describe('shouldWarnCounterPairs', () => {
|
||
const pairs = [pair('a', 'b')];
|
||
const withKey = (v: string | null) => ({ getItem: () => v });
|
||
it('пусто или завершённый статус — false', () => {
|
||
expect(shouldWarnCounterPairs('v', 'CONSOLIDATION_RF', [], withKey(null))).toBe(false);
|
||
for (const s of FINISHED_STATUSES) expect(shouldWarnCounterPairs('v', s, pairs, withKey(null))).toBe(false);
|
||
expect(shouldWarnCounterPairs('v', 'CANCELLED', pairs, withKey(null))).toBe(false);
|
||
});
|
||
it('ключ в хранилище', () => {
|
||
expect(shouldWarnCounterPairs('v', 'CONSOLIDATION_RF', pairs, withKey(null))).toBe(true);
|
||
expect(shouldWarnCounterPairs('v', 'CONSOLIDATION_RF', pairs, withKey('a>b'))).toBe(false);
|
||
expect(shouldWarnCounterPairs('v', 'CONSOLIDATION_RF', pairs, withKey('x>y'))).toBe(true);
|
||
});
|
||
it('null и throwing storage — true', () => {
|
||
expect(shouldWarnCounterPairs('v', 'CONSOLIDATION_RF', pairs, null)).toBe(true);
|
||
const throwing = {
|
||
getItem: () => {
|
||
throw new Error('denied');
|
||
},
|
||
};
|
||
expect(shouldWarnCounterPairs('v', 'CONSOLIDATION_RF', pairs, throwing)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('rememberCounterPairsShown', () => {
|
||
it('пишет ключ', () => {
|
||
const setItem = vi.fn();
|
||
rememberCounterPairsShown('v1', [pair('a', 'b')], { setItem });
|
||
expect(setItem).toHaveBeenCalledWith('armka:counter-pairs:v1', 'a>b');
|
||
});
|
||
it('глотает исключения и null', () => {
|
||
const throwing = {
|
||
setItem: () => {
|
||
throw new Error('denied');
|
||
},
|
||
};
|
||
expect(() => rememberCounterPairsShown('v1', [pair('a', 'b')], throwing)).not.toThrow();
|
||
expect(() => rememberCounterPairsShown('v1', [pair('a', 'b')], null)).not.toThrow();
|
||
});
|
||
});
|
||
|
||
describe('counterPairsWarning', () => {
|
||
it('одна пара у одного участника', () => {
|
||
const w = counterPairsWarning([pair('a', 'b')], 1260);
|
||
expect(w.headline).toBe('Нет встречных записей: 1 пара у 1 участника.');
|
||
expect(w.details.endsWith('Ещё 1260 строк без ИНН учтены как внешние.')).toBe(true);
|
||
});
|
||
it('пять пар у трёх участников (M — разные missing_id)', () => {
|
||
const ps = [pair('a', 'c'), pair('a', 'd'), pair('a', 'e'), pair('b', 'c'), pair('b', 'd')];
|
||
expect(counterPairsWarning(ps, 5).headline).toBe('Нет встречных записей: 5 пар у 3 участников.');
|
||
});
|
||
it('M считает missing_id, а не holder_id', () => {
|
||
const ps = [pair('a', 'b'), pair('a', 'c'), pair('d', 'b'), pair('e', 'b'), pair('f', 'b')];
|
||
expect(counterPairsWarning(ps, 5).headline).toBe('Нет встречных записей: 5 пар у 2 участников.');
|
||
});
|
||
it('две пары у двух участников', () => {
|
||
expect(counterPairsWarning([pair('a', 'b'), pair('a', 'c')], 1).headline).toBe('Нет встречных записей: 2 пары у 2 участников.');
|
||
});
|
||
it('без строк без ИНН', () => {
|
||
expect(counterPairsWarning([pair('a', 'b')], 0).details).toBe(
|
||
'Возможно, контрагент среди строк без ИНН, его ИНН не найден в справочнике или это пропуск в учёте.',
|
||
);
|
||
});
|
||
});
|