Управление заявками #7
@@ -8,7 +8,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
backend-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -38,3 +38,37 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TEST_POSTGRES_CONNECTION: "Host=localhost;Port=5432;Database=tnsu_approval_route;Username=tnsu_approval_route;Password=tnsu_approval_route"
|
TEST_POSTGRES_CONNECTION: "Host=localhost;Port=5432;Database=tnsu_approval_route;Username=tnsu_approval_route;Password=tnsu_approval_route"
|
||||||
run: dotnet test backend/Tnsu.ApprovalRoute.sln --no-build -c Release
|
run: dotnet test backend/Tnsu.ApprovalRoute.sln --no-build -c Release
|
||||||
|
|
||||||
|
frontend-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js 22
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm config set fetch-retries 5
|
||||||
|
npm config set fetch-retry-mintimeout 10000
|
||||||
|
npm config set fetch-retry-maxtimeout 120000
|
||||||
|
for i in 1 2 3; do
|
||||||
|
npm install --legacy-peer-deps && break
|
||||||
|
echo "Attempt $i failed, retrying in 20s..."
|
||||||
|
sleep 20
|
||||||
|
done
|
||||||
|
|
||||||
|
# heft test = build (typescript+sass+lint) и следом Jest — тот же смысл,
|
||||||
|
# что и npm run build, но без package-solution (упаковка .sppkg здесь не
|
||||||
|
# нужна, это часть deploy_frontend.yml, а не проверки на PR).
|
||||||
|
- name: Run frontend unit tests (Jest via heft)
|
||||||
|
run: npx heft test --clean
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { REQUEST_STATUS, isEditableRowStatus, isFullyClosedRequestStatus } from './requestManage';
|
||||||
|
|
||||||
|
describe('isEditableRowStatus', () => {
|
||||||
|
it('черновик/в очереди/на согласовании/на исполнении — редактируемые', () => {
|
||||||
|
expect(isEditableRowStatus(REQUEST_STATUS.draft)).toBe(true);
|
||||||
|
expect(isEditableRowStatus(REQUEST_STATUS.queued)).toBe(true);
|
||||||
|
expect(isEditableRowStatus(REQUEST_STATUS.onApproval)).toBe(true);
|
||||||
|
expect(isEditableRowStatus(REQUEST_STATUS.onExecution)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('"Возвращена на доработку" — редактируемый статус (можно переназначить)', () => {
|
||||||
|
expect(isEditableRowStatus(REQUEST_STATUS.returned)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('согласовано/выполнено/отклонено — финальные решения, НЕ редактируемые', () => {
|
||||||
|
expect(isEditableRowStatus(REQUEST_STATUS.approved)).toBe(false);
|
||||||
|
expect(isEditableRowStatus(REQUEST_STATUS.executed)).toBe(false);
|
||||||
|
expect(isEditableRowStatus(REQUEST_STATUS.rejected)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('незнакомый статус считается нередактируемым (безопасный дефолт)', () => {
|
||||||
|
expect(isEditableRowStatus('какой-то новый статус')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isFullyClosedRequestStatus', () => {
|
||||||
|
it('true только для статуса "Закрыта"', () => {
|
||||||
|
expect(isFullyClosedRequestStatus(REQUEST_STATUS.closed)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false для любого другого статуса заявки, включая "Выполнение"', () => {
|
||||||
|
expect(isFullyClosedRequestStatus(REQUEST_STATUS.execution)).toBe(false);
|
||||||
|
expect(isFullyClosedRequestStatus(REQUEST_STATUS.draft)).toBe(false);
|
||||||
|
expect(isFullyClosedRequestStatus('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { navigate, parseRoute } from './routing';
|
||||||
|
|
||||||
|
// buildQuery/navigate читают/пишут window.location через history API — сбрасываем
|
||||||
|
// адрес перед каждым тестом, чтобы тесты не зависели от порядка запуска.
|
||||||
|
beforeEach(() => {
|
||||||
|
window.history.replaceState({}, '', '/');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseRoute', () => {
|
||||||
|
it('без aatab — вкладка по умолчанию dashboard, всё остальное пусто', () => {
|
||||||
|
expect(parseRoute('')).toEqual({ tab: 'dashboard', routeId: undefined, resolve: undefined, manage: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('незнакомое значение aatab тоже откатывается на dashboard', () => {
|
||||||
|
expect(parseRoute('?aatab=bogus-tab').tab).toBe('dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('manageRequests с aamtype+aamreq — даёт заполненный manage', () => {
|
||||||
|
const route = parseRoute('?aatab=manageRequests&aamtype=5&aamreq=42');
|
||||||
|
expect(route.tab).toBe('manageRequests');
|
||||||
|
expect(route.manage).toEqual({ requestTypeId: '5', requestId: '42' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('manageRequests без aamreq — manage не собирается (id заявки обязателен)', () => {
|
||||||
|
const route = parseRoute('?aatab=manageRequests&aamtype=5');
|
||||||
|
expect(route.manage).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aaroute учитывается только при tab=routes', () => {
|
||||||
|
expect(parseRoute('?aatab=routes&aaroute=7').routeId).toBe(7);
|
||||||
|
expect(parseRoute('?aatab=manageRequests&aaroute=7').routeId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('нечисловой/отсутствующий aaroute — routeId не задан', () => {
|
||||||
|
expect(parseRoute('?aatab=routes&aaroute=not-a-number').routeId).toBeUndefined();
|
||||||
|
expect(parseRoute('?aatab=routes').routeId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolve.fieldValues парсит валидный JSON-массив', () => {
|
||||||
|
const fields = encodeURIComponent(JSON.stringify([{ fieldName: 'Amount', value: '100' }]));
|
||||||
|
const route = parseRoute(`?aatab=resolve&aartype=1&aafields=${fields}`);
|
||||||
|
expect(route.resolve?.fieldValues).toEqual([{ fieldName: 'Amount', value: '100' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolve.fieldValues на битом JSON не роняет парсинг маршрута — просто пустой массив', () => {
|
||||||
|
const route = parseRoute('?aatab=resolve&aartype=1&aafields=not-json');
|
||||||
|
expect(route.resolve?.fieldValues).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolve не собирается без requestTypeId, даже если tab=resolve', () => {
|
||||||
|
expect(parseRoute('?aatab=resolve').resolve).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('navigate', () => {
|
||||||
|
it('после navigate() параметры читаются обратно тем же parseRoute (round-trip)', () => {
|
||||||
|
navigate({ tab: 'manageRequests', manage: { requestTypeId: '3', requestId: '10' } });
|
||||||
|
const roundTripped = parseRoute(window.location.search);
|
||||||
|
expect(roundTripped.tab).toBe('manageRequests');
|
||||||
|
expect(roundTripped.manage).toEqual({ requestTypeId: '3', requestId: '10' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('navigate на dashboard полностью убирает параметры вкладки из адреса', () => {
|
||||||
|
navigate({ tab: 'manageRequests', manage: { requestTypeId: '3', requestId: '10' } });
|
||||||
|
navigate({ tab: 'dashboard' });
|
||||||
|
expect(window.location.search).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('дописывает новую запись в историю (pushState, а не replaceState)', () => {
|
||||||
|
const initialLength = window.history.length;
|
||||||
|
navigate({ tab: 'requestTypes' });
|
||||||
|
expect(window.history.length).toBe(initialLength + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Employee } from '../models';
|
||||||
|
import { findEmployeeByEmail, toEmployeeOption } from './employee';
|
||||||
|
|
||||||
|
const employees: Employee[] = [
|
||||||
|
{ fio: 'Горбенко Павел', email: 'gorbenko.pavel@tnsu.kz', position: 'Разработчик middle' },
|
||||||
|
{ fio: 'Соколов Давид', email: 'sokolov.david@tnsu.kz', position: 'Руководитель отдела' }
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('findEmployeeByEmail', () => {
|
||||||
|
it('находит сотрудника по точному совпадению email', () => {
|
||||||
|
expect(findEmployeeByEmail(employees, 'gorbenko.pavel@tnsu.kz')?.fio).toBe('Горбенко Павел');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('сравнение регистронезависимое', () => {
|
||||||
|
expect(findEmployeeByEmail(employees, 'GORBENKO.PAVEL@TNSU.KZ')?.fio).toBe('Горбенко Павел');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('возвращает undefined, если email не задан', () => {
|
||||||
|
expect(findEmployeeByEmail(employees, undefined)).toBeUndefined();
|
||||||
|
expect(findEmployeeByEmail(employees, '')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('возвращает undefined, если сотрудник с таким email не найден', () => {
|
||||||
|
expect(findEmployeeByEmail(employees, 'nobody@tnsu.kz')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toEmployeeOption', () => {
|
||||||
|
it('строит опцию для SearchableSelect из сотрудника', () => {
|
||||||
|
expect(toEmployeeOption(employees[0])).toEqual({
|
||||||
|
value: 'gorbenko.pavel@tnsu.kz',
|
||||||
|
label: 'Горбенко Павел',
|
||||||
|
sublabel: 'gorbenko.pavel@tnsu.kz'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { describeError, reportError } from './errors';
|
||||||
|
|
||||||
|
describe('describeError', () => {
|
||||||
|
it('вытаскивает читаемый текст из сырого odata.error-ответа SharePoint', () => {
|
||||||
|
const raw =
|
||||||
|
'Error making HttpClient request in queryable [500] :: ' +
|
||||||
|
'{"odata.error":{"code":"-1, Microsoft.SharePoint.SPException","message":{"lang":"ru-RU","value":"Не удалось сохранить."}}}';
|
||||||
|
expect(describeError(new Error(raw))).toBe('Не удалось сохранить.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('раскрывает \\uXXXX-escape внутри value в кириллицу (JSON.parse делает это сам)', () => {
|
||||||
|
const raw = '{"odata.error":{"message":{"lang":"ru-RU","value":"\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442"}}}';
|
||||||
|
expect(describeError(new Error(raw))).toBe('Привет');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('возвращает исходный текст, если в сообщении нет JSON', () => {
|
||||||
|
expect(describeError(new Error('Просто текст ошибки'))).toBe('Просто текст ошибки');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('возвращает исходный текст, если JSON есть, но не парсится (обрезан)', () => {
|
||||||
|
const raw = 'Error :: {"odata.error":{"message":';
|
||||||
|
expect(describeError(new Error(raw))).toBe(raw);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('возвращает исходный текст, если JSON валиден, но без odata.error.message.value', () => {
|
||||||
|
const raw = '{"foo":"bar"}';
|
||||||
|
expect(describeError(new Error(raw))).toBe(raw);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('работает не только с Error, но и с произвольным выброшенным значением', () => {
|
||||||
|
expect(describeError('строка вместо Error')).toBe('строка вместо Error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reportError', () => {
|
||||||
|
it('логирует ошибку целиком в консоль и возвращает читаемый текст', () => {
|
||||||
|
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const error = new Error('{"odata.error":{"message":{"value":"Сбой"}}}');
|
||||||
|
|
||||||
|
const result = reportError(error, 'someContext');
|
||||||
|
|
||||||
|
expect(result).toBe('Сбой');
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(spy.mock.calls[0][0]).toContain('someContext');
|
||||||
|
expect(spy.mock.calls[0][1]).toBe(error);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { IOrderedItem, blockIndexOf, blockRanges, markParallel, swapBlocks, withOrdersFrom } from './orderBlocks';
|
||||||
|
|
||||||
|
interface TestRow extends IOrderedItem {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function row(id: number, order: number, parallelWithPrev?: boolean): TestRow {
|
||||||
|
return { id, order, parallelWithPrev };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('blockRanges', () => {
|
||||||
|
it('кладёт каждую непараллельную строку в отдельный блок', () => {
|
||||||
|
const rows = [row(1, 1), row(2, 2), row(3, 3)];
|
||||||
|
expect(blockRanges(rows)).toEqual([
|
||||||
|
{ start: 0, end: 0 },
|
||||||
|
{ start: 1, end: 1 },
|
||||||
|
{ start: 2, end: 2 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('объединяет подряд идущие parallelWithPrev-строки в один блок', () => {
|
||||||
|
const rows = [row(1, 1), row(2, 2), row(3, 2, true), row(4, 3)];
|
||||||
|
expect(blockRanges(rows)).toEqual([
|
||||||
|
{ start: 0, end: 0 },
|
||||||
|
{ start: 1, end: 2 },
|
||||||
|
{ start: 3, end: 3 }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('игнорирует parallelWithPrev у самой первой строки — блок ей всё равно свой', () => {
|
||||||
|
const rows = [row(1, 1, true)];
|
||||||
|
expect(blockRanges(rows)).toEqual([{ start: 0, end: 0 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('для пустого списка строк возвращает пустой список блоков', () => {
|
||||||
|
expect(blockRanges([])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('blockIndexOf', () => {
|
||||||
|
const ranges = [
|
||||||
|
{ start: 0, end: 0 },
|
||||||
|
{ start: 1, end: 2 },
|
||||||
|
{ start: 3, end: 3 }
|
||||||
|
];
|
||||||
|
|
||||||
|
it('находит блок для строки в его середине/на границах', () => {
|
||||||
|
expect(blockIndexOf(ranges, 0)).toBe(0);
|
||||||
|
expect(blockIndexOf(ranges, 1)).toBe(1);
|
||||||
|
expect(blockIndexOf(ranges, 2)).toBe(1);
|
||||||
|
expect(blockIndexOf(ranges, 3)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('возвращает -1, если индекс строки ни в один диапазон не попадает', () => {
|
||||||
|
expect(blockIndexOf(ranges, 99)).toBe(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('swapBlocks', () => {
|
||||||
|
it('меняет местами два блока, не трогая порядок строк внутри них', () => {
|
||||||
|
const rows = ['a', 'b1', 'b2', 'c'];
|
||||||
|
const a = { start: 0, end: 0 };
|
||||||
|
const b = { start: 1, end: 2 };
|
||||||
|
expect(swapBlocks(rows, a, b)).toEqual(['b1', 'b2', 'a', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('не трогает строки за пределами переставляемых блоков', () => {
|
||||||
|
const rows = ['head', 'a', 'b', 'tail'];
|
||||||
|
const a = { start: 1, end: 1 };
|
||||||
|
const b = { start: 2, end: 2 };
|
||||||
|
expect(swapBlocks(rows, a, b)).toEqual(['head', 'b', 'a', 'tail']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('withOrdersFrom', () => {
|
||||||
|
it('нумерует непараллельные строки последовательно начиная с floor+1', () => {
|
||||||
|
const rows = [row(1, 5), row(2, 5), row(3, 5)];
|
||||||
|
const result = withOrdersFrom(rows, 0);
|
||||||
|
expect(result.map(r => r.order)).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('строкам с parallelWithPrev даёт тот же order, что и предыдущей строке', () => {
|
||||||
|
const rows = [row(1, 1), row(2, 1, true), row(3, 1)];
|
||||||
|
const result = withOrdersFrom(rows, 0);
|
||||||
|
expect(result.map(r => r.order)).toEqual([1, 1, 2]);
|
||||||
|
expect(result.map(r => r.parallelWithPrev)).toEqual([false, true, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('первая строка никогда не параллельна, даже если parallelWithPrev выставлен явно', () => {
|
||||||
|
const rows = [row(1, 1, true), row(2, 1)];
|
||||||
|
const result = withOrdersFrom(rows, 0);
|
||||||
|
expect(result[0].parallelWithPrev).toBe(false);
|
||||||
|
expect(result[0].order).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('floor сдвигает нумерацию — строки после заблокированного шага не могут получить order <= floor', () => {
|
||||||
|
const rows = [row(1, 1), row(2, 2)];
|
||||||
|
const result = withOrdersFrom(rows, 3);
|
||||||
|
expect(result.map(r => r.order)).toEqual([4, 5]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('markParallel', () => {
|
||||||
|
it('помечает строку как параллельную, только если её order совпадает с order предыдущей', () => {
|
||||||
|
const rows = [row(1, 1), row(2, 1), row(3, 2), row(4, 2)];
|
||||||
|
const result = markParallel(rows);
|
||||||
|
expect(result.map(r => r.parallelWithPrev)).toEqual([false, true, false, true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('первая строка списка никогда не параллельна', () => {
|
||||||
|
const rows = [row(1, 1)];
|
||||||
|
expect(markParallel(rows)[0].parallelWithPrev).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { ComparisonOperator, Employee, HeadType, ProjectRole, RouteStepStatus, TemplateRouteStep } from '../models';
|
||||||
|
import { describeStepCondition, describeStepExecutor, detectExecutorKind } from './stepExecutor';
|
||||||
|
|
||||||
|
function step(overrides: Partial<TemplateRouteStep>): TemplateRouteStep {
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
orderNo: 1,
|
||||||
|
executorEmail: null,
|
||||||
|
position: null,
|
||||||
|
projectRole: null,
|
||||||
|
headType: null,
|
||||||
|
status: RouteStepStatus.Approval,
|
||||||
|
routeId: 1,
|
||||||
|
allowReassignToOtherApprovers: false,
|
||||||
|
fieldName: null,
|
||||||
|
operation: null,
|
||||||
|
value: null,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('detectExecutorKind', () => {
|
||||||
|
it('email имеет приоритет над остальными способами', () => {
|
||||||
|
expect(detectExecutorKind({ executorEmail: 'a@tnsu.kz', position: 'Директор' })).toBe('email');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('при отсутствии email — position', () => {
|
||||||
|
expect(detectExecutorKind({ position: 'Директор' })).toBe('position');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('при отсутствии email/position — projectRole, если он не None', () => {
|
||||||
|
expect(detectExecutorKind({ projectRole: ProjectRole.Planner })).toBe('projectRole');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('projectRole=None не считается заданным способом', () => {
|
||||||
|
expect(detectExecutorKind({ projectRole: ProjectRole.None })).toBe('headType');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('если ничего не задано — headType (последний вариант по умолчанию)', () => {
|
||||||
|
expect(detectExecutorKind({})).toBe('headType');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('describeStepExecutor', () => {
|
||||||
|
const employees: Employee[] = [{ fio: 'Горбенко Павел', email: 'gorbenko.pavel@tnsu.kz', position: 'Разработчик' }];
|
||||||
|
|
||||||
|
it('для email найденного сотрудника показывает ФИО и email отдельной строкой', () => {
|
||||||
|
const result = describeStepExecutor(step({ executorEmail: 'gorbenko.pavel@tnsu.kz' }), employees);
|
||||||
|
expect(result).toEqual({ primary: 'Горбенко Павел', secondary: 'gorbenko.pavel@tnsu.kz' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('для email, которого нет в справочнике, показывает сам email без secondary', () => {
|
||||||
|
const result = describeStepExecutor(step({ executorEmail: 'unknown@tnsu.kz' }), employees);
|
||||||
|
expect(result).toEqual({ primary: 'unknown@tnsu.kz' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('для шага по должности показывает должность', () => {
|
||||||
|
const result = describeStepExecutor(step({ position: 'Главный бухгалтер' }), employees);
|
||||||
|
expect(result.primary).toBe('Главный бухгалтер');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('для шага по проектной роли показывает подпись роли', () => {
|
||||||
|
const result = describeStepExecutor(step({ projectRole: ProjectRole.Planner }), employees);
|
||||||
|
expect(result.primary).toEqual(expect.stringContaining('Роль:'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('для шага по типу руководителя показывает подпись типа', () => {
|
||||||
|
const result = describeStepExecutor(step({ headType: HeadType.Chief }), employees);
|
||||||
|
expect(result.primary).toEqual(expect.stringContaining('Руководитель:'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('если у шага вообще не задан исполнитель — пустое значение', () => {
|
||||||
|
const result = describeStepExecutor(step({}), employees);
|
||||||
|
expect(result.secondary).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('describeStepCondition', () => {
|
||||||
|
it('undefined, если условие не задано целиком', () => {
|
||||||
|
expect(describeStepCondition(step({}))).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('undefined, если operation === None', () => {
|
||||||
|
expect(describeStepCondition(step({ fieldName: 'Amount', operation: ComparisonOperator.None, value: '100' }))).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('собирает "поле оператор значение", если условие задано полностью', () => {
|
||||||
|
const result = describeStepCondition(step({ fieldName: 'Amount', operation: ComparisonOperator.GreaterThan, value: '100' }));
|
||||||
|
expect(result).toBe('Amount > 100');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user