test: добавляет Vitest и CI-workflow по образцу request-portal

Покрыты чистые функции бизнес-логики: validateFeedback (обязательность полей
по видам обращения, минимальная длина текста, анонимность) и
getAppealFieldsConfig (согласованность конфига — обязательное поле не может
быть скрыто). React-компоненты пока не тестируются: сначала нужно решить,
куда форма отправляет данные, иначе тесты придётся сразу переписывать.

vite.config.ts: defineConfig теперь из 'vitest/config' — один файл конфига
на сборку и тесты вместо двух рассинхронизирующихся. globals: true и
environment: 'node', т.к. тестируется логика, а не DOM.

.gitea/workflows/test.yml — тот же принцип, что в request-portal (push в
main, PR, ретраи npm install), но npm run ci вместо heft test: здесь нет
SPFx/Jest, и деплой пока не добавлен — куда разворачивать dist/, не решено.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 09:45:05 +05:00
parent 05cd491769
commit 3cf46fa55c
8 changed files with 626 additions and 10 deletions
@@ -0,0 +1,122 @@
import { EMPTY_FEEDBACK, type IFeedbackData } from './model';
import { TEXTS } from './texts';
import { validateFeedback } from './validateFeedback';
const VALID_COMPANY = '9de064a4-44c8-88ef-11ec-8285925d83d2';
const LONG_TEXT = 'Развёрнутое описание, которого достаточно для проверки правила';
const SHORT_TEXT = 'коротко';
/** Полностью заполненное «жалоба»-обращение — стартовая точка для тестов. */
function buildValidComplaint(overrides: Partial<IFeedbackData> = {}): IFeedbackData {
return {
...EMPTY_FEEDBACK,
company: VALID_COMPANY,
fullName: 'Иванов Иван Иванович',
kind: 'complaint',
situation: LONG_TEXT,
suggestion: LONG_TEXT,
...overrides
};
}
describe('validateFeedback: базовые обязательные поля', () => {
it('полностью заполненное обращение проходит без ошибок', () => {
expect(validateFeedback(buildValidComplaint())).toEqual({});
});
it('без компании — ошибка', () => {
const errors = validateFeedback(buildValidComplaint({ company: '' }));
expect(errors.company).toBeDefined();
});
it('именное обращение без ФИО — ошибка', () => {
const errors = validateFeedback(buildValidComplaint({ fullName: '' }));
expect(errors.fullName).toBeDefined();
});
it('анонимное обращение без ФИО — не ошибка (поле скрыто, требовать его нельзя)', () => {
const errors = validateFeedback(
buildValidComplaint({ isAnonymous: true, fullName: '' })
);
expect(errors.fullName).toBeUndefined();
});
});
describe('validateFeedback: вид обращения "Другое"', () => {
it('без уточнения вида — ошибка', () => {
const errors = validateFeedback(
buildValidComplaint({ kind: 'other', kindOther: '', situation: LONG_TEXT })
);
expect(errors.kindOther).toBeDefined();
});
it('с уточнением — ошибки по kindOther нет', () => {
const errors = validateFeedback(
buildValidComplaint({ kind: 'other', kindOther: 'Свой вариант', situation: LONG_TEXT })
);
expect(errors.kindOther).toBeUndefined();
});
});
// Состав обязательных полей зависит от вида обращения (см. getAppealFieldsConfig
// в model.ts) — эти тесты фиксируют контракт, чтобы форма и валидация не разошлись.
describe('validateFeedback: обязательность полей по видам обращения', () => {
it('жалоба: описание ситуации обязательно', () => {
const errors = validateFeedback(buildValidComplaint({ situation: '' }));
expect(errors.situation).toBeDefined();
});
it('жалоба: предложение по улучшению НЕ обязательно', () => {
const errors = validateFeedback(buildValidComplaint({ suggestion: '' }));
expect(errors.suggestion).toBeUndefined();
});
it('жалоба: если предложение всё же указано, но слишком коротко — ошибка', () => {
const errors = validateFeedback(buildValidComplaint({ suggestion: SHORT_TEXT }));
expect(errors.suggestion).toBeDefined();
});
it('предложение: описание ситуации не требуется, даже если пустое', () => {
const errors = validateFeedback(
buildValidComplaint({ kind: 'suggestion', situation: '', suggestion: LONG_TEXT })
);
expect(errors.situation).toBeUndefined();
});
it('предложение: сама суть предложения обязательна', () => {
const errors = validateFeedback(
buildValidComplaint({ kind: 'suggestion', situation: '', suggestion: '' })
);
expect(errors.suggestion).toBeDefined();
});
it('другое: описание обязательно, предложение не спрашивается вовсе', () => {
const errors = validateFeedback(
buildValidComplaint({
kind: 'other',
kindOther: 'Свой вариант',
situation: '',
suggestion: ''
})
);
expect(errors.situation).toBeDefined();
expect(errors.suggestion).toBeUndefined();
});
});
describe('validateFeedback: минимальная длина текста', () => {
it('пустое описание — ошибка «заполните поле»', () => {
const errors = validateFeedback(buildValidComplaint({ situation: '' }));
expect(errors.situation).toBe(TEXTS.errors.required);
});
it('слишком короткое описание — ошибка «коротко», а не «пусто»', () => {
const errors = validateFeedback(buildValidComplaint({ situation: SHORT_TEXT }));
expect(errors.situation).toBe(TEXTS.errors.tooShort);
});
it('достаточно длинное описание — без ошибки', () => {
const errors = validateFeedback(buildValidComplaint({ situation: LONG_TEXT }));
expect(errors.situation).toBeUndefined();
});
});