623 lines
29 KiB
TypeScript
623 lines
29 KiB
TypeScript
import {
|
||
IRoute,
|
||
navigate,
|
||
navigateToRequestLink,
|
||
newRequestLinkUrl,
|
||
parseRoute,
|
||
persistOwnStatus,
|
||
persistOwnTab,
|
||
persistPage,
|
||
persistQuery,
|
||
requestLinkUrl,
|
||
routeUrl,
|
||
storedOwnStatus,
|
||
storedOwnTab,
|
||
storedPageFor,
|
||
storedQueryFor,
|
||
} from './routing';
|
||
|
||
// buildQuery/navigate читают и пишут window.location через history API, а
|
||
// подвкладка/фильтр «Моих заявок» — ещё и localStorage (см. persistOwnTab/
|
||
// persistOwnStatus) — сбрасываем оба перед каждым тестом, чтобы они не
|
||
// зависели от порядка запуска.
|
||
beforeEach(() => {
|
||
window.history.replaceState({}, '', '/');
|
||
window.localStorage.clear();
|
||
});
|
||
|
||
describe('parseRoute', () => {
|
||
it('пустой адрес — подача заявки в разделе «Новая заявка»', () => {
|
||
// page всегда определён (1 — значение по умолчанию), в адрес не пишется.
|
||
expect(parseRoute('')).toEqual({ mode: 'submit', nav: 'new', page: 1, query: '' });
|
||
});
|
||
|
||
it('rpnav задаёт раздел бокового меню', () => {
|
||
expect(parseRoute('?rpnav=approvals').nav).toBe('approvals');
|
||
expect(parseRoute('?rpnav=execution').nav).toBe('execution');
|
||
expect(parseRoute('?rpnav=rework').nav).toBe('rework');
|
||
expect(parseRoute('?rpnav=own').nav).toBe('own');
|
||
expect(parseRoute('?rpnav=search').nav).toBe('search');
|
||
});
|
||
|
||
it('незнакомый rpnav откатывается на раздел по умолчанию', () => {
|
||
expect(parseRoute('?rpnav=bogus').nav).toBe('new');
|
||
});
|
||
|
||
it('полный набор параметров открывает заявку на согласование', () => {
|
||
expect(parseRoute('?rpmode=approve&rptype=hire&rpid=42')).toEqual({
|
||
mode: 'approve',
|
||
nav: 'new',
|
||
formKey: 'hire',
|
||
itemId: 42,
|
||
page: 1,
|
||
query: '',
|
||
});
|
||
});
|
||
|
||
it('раздел меню сохраняется, когда поверх него открыта заявка', () => {
|
||
expect(parseRoute('?rpmode=approve&rptype=hire&rpid=42&rpnav=own').nav).toBe('own');
|
||
});
|
||
|
||
it('режим редактирования собственной заявки разбирается так же, как согласование', () => {
|
||
expect(parseRoute('?rpmode=edit&rptype=hire&rpid=7')).toEqual({
|
||
mode: 'edit',
|
||
nav: 'new',
|
||
formKey: 'hire',
|
||
itemId: 7,
|
||
page: 1,
|
||
query: '',
|
||
});
|
||
});
|
||
|
||
it('без rpid заявка не открывается — остаётся обычная подача', () => {
|
||
expect(parseRoute('?rpmode=approve&rptype=hire').mode).toBe('submit');
|
||
});
|
||
|
||
it('нечисловой rpid не открывает заявку', () => {
|
||
expect(parseRoute('?rpmode=approve&rptype=hire&rpid=abc').mode).toBe('submit');
|
||
});
|
||
|
||
it('rpid=0 не считается заявкой (в SharePoint нумерация с единицы)', () => {
|
||
expect(parseRoute('?rpmode=approve&rptype=hire&rpid=0').mode).toBe('submit');
|
||
});
|
||
|
||
it('rptype + rpsource — создание производной заявки с предзаполнением', () => {
|
||
expect(parseRoute('?rptype=workplace&rpsource=15')).toEqual({
|
||
mode: 'create',
|
||
nav: 'new',
|
||
formKey: 'workplace',
|
||
sourceId: 15,
|
||
});
|
||
});
|
||
|
||
it('создание производной заявки всегда открывается в разделе «Новая заявка»', () => {
|
||
expect(parseRoute('?rptype=workplace&rpsource=15&rpnav=own').nav).toBe('new');
|
||
});
|
||
|
||
it('открытая заявка имеет приоритет над предзаполнением', () => {
|
||
expect(parseRoute('?rpmode=approve&rptype=hire&rpid=42&rpsource=15').mode).toBe('approve');
|
||
});
|
||
|
||
it('rpmode=create + rptype без rpsource — пустая новая форма без предзаполнения', () => {
|
||
expect(parseRoute('?rpmode=create&rptype=hire')).toEqual({
|
||
mode: 'create',
|
||
nav: 'new',
|
||
formKey: 'hire',
|
||
});
|
||
});
|
||
|
||
it('rptype БЕЗ rpmode — тоже пустая новая форма (buildQuery для create его не пишет)', () => {
|
||
expect(parseRoute('?rptype=hire')).toEqual({
|
||
mode: 'create',
|
||
nav: 'new',
|
||
formKey: 'hire',
|
||
});
|
||
});
|
||
|
||
it('rpmode=create без rptype не открывает пустую форму — остаётся обычная подача', () => {
|
||
expect(parseRoute('?rpmode=create').mode).toBe('submit');
|
||
});
|
||
|
||
it('внешняя ссылка ТМЦ: rptype + positionId + nomenclatureId + storeName прокидываются в create', () => {
|
||
expect(parseRoute('?rptype=wh-tmc-transfer&positionId=00d31059-1f52-11ec-a738-3cecef292546&nomenclatureId=abc-123&storeName=Склад%20№1')).toEqual({
|
||
mode: 'create',
|
||
nav: 'new',
|
||
formKey: 'wh-tmc-transfer',
|
||
positionId: '00d31059-1f52-11ec-a738-3cecef292546',
|
||
nomenclatureId: 'abc-123',
|
||
storeName: 'Склад №1',
|
||
});
|
||
});
|
||
|
||
it('positionId/nomenclatureId без rptype игнорируются — обычная подача', () => {
|
||
// Внешние параметры имеют смысл только при открытии формы (rptype); сами по
|
||
// себе не должны влиять на маршрут.
|
||
expect(parseRoute('?positionId=x&nomenclatureId=y').mode).toBe('submit');
|
||
});
|
||
|
||
it('rpmode=create + rptype + rpsource — предзаполнение важнее пустой формы', () => {
|
||
expect(parseRoute('?rpmode=create&rptype=hire&rpsource=15')).toEqual({
|
||
mode: 'create',
|
||
nav: 'new',
|
||
formKey: 'hire',
|
||
sourceId: 15,
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('newRequestLinkUrl', () => {
|
||
it('строит абсолютную ссылку на пустую форму без rpsource', () => {
|
||
const url = newRequestLinkUrl('hire');
|
||
expect(url).toBe(`${window.location.origin}${window.location.pathname}?rpmode=create&rptype=hire`);
|
||
});
|
||
|
||
it('ссылка на пустую форму разбирается обратно в тот же маршрут', () => {
|
||
const url = newRequestLinkUrl('hire');
|
||
const route = parseRoute(new URL(url).search);
|
||
expect(route).toEqual({ mode: 'create', nav: 'new', formKey: 'hire' });
|
||
});
|
||
});
|
||
|
||
describe('подвкладки и фильтр «Моих заявок»', () => {
|
||
it('без rptab — подвкладка по умолчанию', () => {
|
||
expect(parseRoute('?rpnav=own').ownTab).toBe('created');
|
||
});
|
||
|
||
it('rptab задаёт подвкладку', () => {
|
||
expect(parseRoute('?rpnav=own&rptab=approved').ownTab).toBe('approved');
|
||
expect(parseRoute('?rpnav=own&rptab=rejected').ownTab).toBe('rejected');
|
||
expect(parseRoute('?rpnav=own&rptab=executed').ownTab).toBe('executed');
|
||
});
|
||
|
||
it('незнакомый rptab откатывается на подвкладку по умолчанию', () => {
|
||
expect(parseRoute('?rpnav=own&rptab=bogus').ownTab).toBe('created');
|
||
});
|
||
|
||
it('вне «Моих заявок» подвкладка не читается — чужой rptab туда не протекает', () => {
|
||
expect(parseRoute('?rpnav=approvals&rptab=approved').ownTab).toBeUndefined();
|
||
expect(parseRoute('?rpnav=search&rptab=approved').ownTab).toBeUndefined();
|
||
});
|
||
|
||
it('фильтр по статусу действует только на вкладке «Инициированные»', () => {
|
||
expect(parseRoute('?rpnav=own&rpstatus=Черновик').ownStatus).toBe('Черновик');
|
||
// У вкладок с решениями отбор задаёт сама вкладка — фильтр там бессмыслен.
|
||
expect(parseRoute('?rpnav=own&rptab=approved&rpstatus=Черновик').ownStatus).toBeUndefined();
|
||
expect(parseRoute('?rpnav=approvals&rpstatus=Черновик').ownStatus).toBeUndefined();
|
||
});
|
||
|
||
it('значения по умолчанию не засоряют адрес', () => {
|
||
expect(routeUrl({ mode: 'submit', nav: 'own', ownTab: 'created' })).not.toContain('rptab');
|
||
expect(routeUrl({ mode: 'submit', nav: 'own' })).not.toContain('rptab');
|
||
});
|
||
|
||
it('непустая подвкладка и фильтр попадают в адрес', () => {
|
||
expect(routeUrl({ mode: 'submit', nav: 'own', ownTab: 'rejected' })).toContain('rptab=rejected');
|
||
expect(routeUrl({ mode: 'submit', nav: 'own', ownTab: 'created', ownStatus: 'Черновик' })).toContain('rpstatus=');
|
||
});
|
||
|
||
it('фильтр не пишется в адрес вместе с подвкладкой решений', () => {
|
||
const url = routeUrl({ mode: 'submit', nav: 'own', ownTab: 'approved', ownStatus: 'Черновик' });
|
||
expect(url).toContain('rptab=approved');
|
||
expect(url).not.toContain('rpstatus');
|
||
});
|
||
|
||
it('уход из «Моих заявок» убирает подвкладку и фильтр из адреса', () => {
|
||
navigate({ mode: 'submit', nav: 'own', ownTab: 'rejected' });
|
||
navigate({ mode: 'submit', nav: 'approvals' });
|
||
expect(window.location.search).not.toContain('rptab');
|
||
expect(window.location.search).not.toContain('rpstatus');
|
||
});
|
||
});
|
||
|
||
describe('страница таблицы (rppage + persistPage)', () => {
|
||
it('без rppage — первая страница', () => {
|
||
expect(parseRoute('?rpnav=own').page).toBe(1);
|
||
});
|
||
|
||
it('rppage задаёт страницу', () => {
|
||
expect(parseRoute('?rpnav=own&rppage=3').page).toBe(3);
|
||
});
|
||
|
||
it('мусор в rppage трактуется как первая страница, а не роняет разбор', () => {
|
||
expect(parseRoute('?rpnav=own&rppage=0').page).toBe(1);
|
||
expect(parseRoute('?rpnav=own&rppage=-2').page).toBe(1);
|
||
expect(parseRoute('?rpnav=own&rppage=abc').page).toBe(1);
|
||
expect(parseRoute('?rpnav=own&rppage=1.5').page).toBe(1);
|
||
});
|
||
|
||
it('первая страница не засоряет адрес, непервая — попадает', () => {
|
||
expect(routeUrl({ mode: 'submit', nav: 'own', page: 1 })).not.toContain('rppage');
|
||
expect(routeUrl({ mode: 'submit', nav: 'own' })).not.toContain('rppage');
|
||
expect(routeUrl({ mode: 'submit', nav: 'own', page: 4 })).toContain('rppage=4');
|
||
});
|
||
|
||
it('round-trip страницы через navigate', () => {
|
||
navigate({ mode: 'submit', nav: 'approvals', page: 2 });
|
||
expect(parseRoute(window.location.search).page).toBe(2);
|
||
});
|
||
|
||
it('без rppage подставляется запомненная для ЭТОЙ таблицы страница', () => {
|
||
persistPage({ mode: 'submit', nav: 'approvals' }, 5);
|
||
expect(parseRoute('?rpnav=approvals').page).toBe(5);
|
||
});
|
||
|
||
it('страница запоминается отдельно по таблицам и не переносится между разделами', () => {
|
||
persistPage({ mode: 'submit', nav: 'approvals' }, 5);
|
||
// В другом разделе своя нумерация — чужая страница туда попасть не должна,
|
||
// иначе можно приземлиться на несуществующую (у него всего одна страница).
|
||
expect(parseRoute('?rpnav=execution').page).toBe(1);
|
||
});
|
||
|
||
it('подвкладки «Моих заявок» — тоже разные таблицы, у каждой своя страница', () => {
|
||
persistPage({ mode: 'submit', nav: 'own', ownTab: 'created' }, 3);
|
||
persistPage({ mode: 'submit', nav: 'own', ownTab: 'approved' }, 7);
|
||
|
||
expect(parseRoute('?rpnav=own&rptab=created').page).toBe(3);
|
||
expect(parseRoute('?rpnav=own&rptab=approved').page).toBe(7);
|
||
});
|
||
|
||
it('явная страница в адресе имеет приоритет над запомненной', () => {
|
||
persistPage({ mode: 'submit', nav: 'approvals' }, 5);
|
||
expect(parseRoute('?rpnav=approvals&rppage=2').page).toBe(2);
|
||
});
|
||
|
||
it('возврат на первую страницу стирает запомненную', () => {
|
||
persistPage({ mode: 'submit', nav: 'approvals' }, 5);
|
||
expect(parseRoute('?rpnav=approvals').page).toBe(5);
|
||
|
||
persistPage({ mode: 'submit', nav: 'approvals' }, 1);
|
||
expect(parseRoute('?rpnav=approvals').page).toBe(1);
|
||
});
|
||
|
||
it('страница переживает открытие заявки поверх списка', () => {
|
||
navigate({ mode: 'submit', nav: 'own', ownTab: 'created', page: 3 });
|
||
navigate({ mode: 'approve', nav: 'own', ownTab: 'created', formKey: 'hire', itemId: 42, page: 3 });
|
||
expect(parseRoute(window.location.search).page).toBe(3);
|
||
});
|
||
|
||
// Ради этого storedPageFor и заведён: при переходе по меню маршрут СТРОИТСЯ,
|
||
// и если не подставить запомненную страницу явно, она останется только в
|
||
// состоянии — в адресе будет первая, а на экране открытая (см. goto в
|
||
// RequestPortal.tsx).
|
||
it('storedPageFor отдаёт запомненную страницу нужной таблицы', () => {
|
||
persistPage({ mode: 'submit', nav: 'approvals' }, 4);
|
||
expect(storedPageFor('approvals')).toBe(4);
|
||
expect(storedPageFor('execution')).toBe(1);
|
||
});
|
||
|
||
it('storedPageFor различает подвкладки «Моих заявок»', () => {
|
||
persistPage({ mode: 'submit', nav: 'own', ownTab: 'rejected' }, 6);
|
||
expect(storedPageFor('own', 'rejected')).toBe(6);
|
||
expect(storedPageFor('own', 'created')).toBe(1);
|
||
});
|
||
|
||
it('storedPageFor без подвкладки берёт запомненную — ту, что и откроется', () => {
|
||
persistOwnTab('executed');
|
||
persistPage({ mode: 'submit', nav: 'own', ownTab: 'executed' }, 2);
|
||
expect(storedPageFor('own')).toBe(2);
|
||
});
|
||
|
||
it('переход по меню кладёт запомненную страницу в адрес', () => {
|
||
persistPage({ mode: 'submit', nav: 'approvals' }, 3);
|
||
navigate({ mode: 'submit', nav: 'approvals', page: storedPageFor('approvals') });
|
||
expect(window.location.search).toContain('rppage=3');
|
||
});
|
||
|
||
it('localStorage недоступен — страница просто первая, без падения', () => {
|
||
const original = window.localStorage.getItem;
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- имитация приватного режима, где чтение localStorage бросает
|
||
(window.localStorage as any).getItem = (): never => {
|
||
throw new Error('SecurityError');
|
||
};
|
||
try {
|
||
expect(() => parseRoute('?rpnav=own')).not.toThrow();
|
||
expect(parseRoute('?rpnav=own').page).toBe(1);
|
||
} finally {
|
||
window.localStorage.getItem = original;
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('поиск (rpq + persistQuery)', () => {
|
||
it('без rpq — пустой запрос', () => {
|
||
expect(parseRoute('?rpnav=own').query).toBe('');
|
||
});
|
||
|
||
it('rpq задаёт запрос, в т.ч. с пробелами и кириллицей', () => {
|
||
expect(parseRoute('?rpnav=own&rpq=%D0%B0%D0%B1%D0%B2%20%D0%B3').query).toBe('абв г');
|
||
});
|
||
|
||
it('пустой запрос не засоряет адрес, непустой — попадает', () => {
|
||
expect(routeUrl({ mode: 'submit', nav: 'own', query: '' })).not.toContain('rpq');
|
||
expect(routeUrl({ mode: 'submit', nav: 'own' })).not.toContain('rpq');
|
||
expect(routeUrl({ mode: 'submit', nav: 'own', query: 'бухгалтер' })).toContain('rpq=');
|
||
});
|
||
|
||
it('round-trip запроса через navigate', () => {
|
||
navigate({ mode: 'submit', nav: 'search', query: 'Горбенко' });
|
||
expect(parseRoute(window.location.search).query).toBe('Горбенко');
|
||
});
|
||
|
||
it('без rpq подставляется запомненный для ЭТОЙ таблицы запрос', () => {
|
||
persistQuery({ mode: 'submit', nav: 'search' }, 'бухгалтер');
|
||
expect(parseRoute('?rpnav=search').query).toBe('бухгалтер');
|
||
});
|
||
|
||
it('запрос запоминается отдельно по таблицам и не переносится между разделами', () => {
|
||
persistQuery({ mode: 'submit', nav: 'search' }, 'бухгалтер');
|
||
expect(parseRoute('?rpnav=approvals').query).toBe('');
|
||
});
|
||
|
||
it('подвкладки «Моих заявок» — тоже разные таблицы, у каждой свой запрос', () => {
|
||
persistQuery({ mode: 'submit', nav: 'own', ownTab: 'created' }, 'приём');
|
||
persistQuery({ mode: 'submit', nav: 'own', ownTab: 'approved' }, 'место');
|
||
|
||
expect(parseRoute('?rpnav=own&rptab=created').query).toBe('приём');
|
||
expect(parseRoute('?rpnav=own&rptab=approved').query).toBe('место');
|
||
});
|
||
|
||
it('очистка запроса стирает запомненный', () => {
|
||
persistQuery({ mode: 'submit', nav: 'search' }, 'бухгалтер');
|
||
expect(parseRoute('?rpnav=search').query).toBe('бухгалтер');
|
||
|
||
persistQuery({ mode: 'submit', nav: 'search' }, '');
|
||
expect(parseRoute('?rpnav=search').query).toBe('');
|
||
});
|
||
|
||
it('storedQueryFor отдаёт запомненный запрос нужной таблицы', () => {
|
||
persistQuery({ mode: 'submit', nav: 'approvals' }, 'заявка');
|
||
expect(storedQueryFor('approvals')).toBe('заявка');
|
||
expect(storedQueryFor('execution')).toBe('');
|
||
});
|
||
|
||
it('переход по меню кладёт восстановленный запрос в адрес', () => {
|
||
persistQuery({ mode: 'submit', nav: 'approvals' }, 'заявка');
|
||
navigate({ mode: 'submit', nav: 'approvals', query: storedQueryFor('approvals') || undefined });
|
||
expect(parseRoute(window.location.search).query).toBe('заявка');
|
||
});
|
||
|
||
it('replace не добавляет запись в историю — набор текста не засоряет «назад»', () => {
|
||
navigate({ mode: 'submit', nav: 'search' });
|
||
const before = window.history.length;
|
||
|
||
navigate({ mode: 'submit', nav: 'search', query: 'бух' }, { replace: true });
|
||
navigate({ mode: 'submit', nav: 'search', query: 'бухг' }, { replace: true });
|
||
|
||
expect(window.history.length).toBe(before);
|
||
expect(parseRoute(window.location.search).query).toBe('бухг');
|
||
});
|
||
|
||
it('localStorage недоступен — запрос просто пустой, без падения', () => {
|
||
const original = window.localStorage.getItem;
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- имитация приватного режима, где чтение localStorage бросает
|
||
(window.localStorage as any).getItem = (): never => {
|
||
throw new Error('SecurityError');
|
||
};
|
||
try {
|
||
expect(() => parseRoute('?rpnav=own')).not.toThrow();
|
||
expect(parseRoute('?rpnav=own').query).toBe('');
|
||
} finally {
|
||
window.localStorage.getItem = original;
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('persistOwnTab/persistOwnStatus — запоминание подвкладки/фильтра между визитами', () => {
|
||
it('без явного rptab в адресе подставляется запомненная подвкладка', () => {
|
||
persistOwnTab('rejected');
|
||
expect(parseRoute('?rpnav=own').ownTab).toBe('rejected');
|
||
});
|
||
|
||
it('без явного rpstatus подставляется запомненный фильтр (только на «Инициированных»)', () => {
|
||
persistOwnStatus('Черновик');
|
||
expect(parseRoute('?rpnav=own').ownStatus).toBe('Черновик');
|
||
// На вкладках с решениями запомненный фильтр ни при чём — там его нет по смыслу.
|
||
persistOwnTab('approved');
|
||
expect(parseRoute('?rpnav=own').ownStatus).toBeUndefined();
|
||
});
|
||
|
||
it('явный параметр в адресе имеет приоритет над запомненным значением', () => {
|
||
persistOwnTab('rejected');
|
||
expect(parseRoute('?rpnav=own&rptab=executed').ownTab).toBe('executed');
|
||
});
|
||
|
||
it('очистка фильтра persistOwnStatus убирает его и из последующих parseRoute', () => {
|
||
persistOwnStatus('Черновик');
|
||
expect(parseRoute('?rpnav=own').ownStatus).toBe('Черновик');
|
||
|
||
persistOwnStatus(undefined);
|
||
expect(parseRoute('?rpnav=own').ownStatus).toBeUndefined();
|
||
});
|
||
|
||
it('запомненное значение не протекает за пределы раздела «Мои заявки»', () => {
|
||
persistOwnTab('rejected');
|
||
expect(parseRoute('?rpnav=approvals').ownTab).toBeUndefined();
|
||
});
|
||
|
||
// Как и со страницей: при переходе маршрут СТРОИТСЯ, и без явной подстановки
|
||
// восстановленный фильтр остался бы только в состоянии — в адресе его бы не
|
||
// было (см. goto/gotoOwnTab в RequestPortal.tsx).
|
||
it('storedOwnStatus отдаёт фильтр только для вкладки «Инициированные»', () => {
|
||
persistOwnStatus('Отклонено');
|
||
expect(storedOwnStatus('created')).toBe('Отклонено');
|
||
expect(storedOwnStatus('approved')).toBeUndefined();
|
||
expect(storedOwnStatus('rejected')).toBeUndefined();
|
||
});
|
||
|
||
it('storedOwnTab отдаёт запомненную подвкладку', () => {
|
||
persistOwnTab('executed');
|
||
expect(storedOwnTab()).toBe('executed');
|
||
});
|
||
|
||
it('переход в «Мои заявки» кладёт восстановленный фильтр в адрес', () => {
|
||
persistOwnStatus('Согласовано');
|
||
const ownTab = storedOwnTab();
|
||
|
||
navigate({ mode: 'submit', nav: 'own', ownTab, ownStatus: storedOwnStatus(ownTab) });
|
||
|
||
expect(window.location.search).toContain('rpstatus=');
|
||
expect(parseRoute(window.location.search).ownStatus).toBe('Согласовано');
|
||
});
|
||
|
||
it('переход на подвкладку решений фильтр в адрес не тащит', () => {
|
||
persistOwnStatus('Согласовано');
|
||
|
||
navigate({ mode: 'submit', nav: 'own', ownTab: 'approved', ownStatus: storedOwnStatus('approved') });
|
||
|
||
expect(window.location.search).toContain('rptab=approved');
|
||
expect(window.location.search).not.toContain('rpstatus');
|
||
});
|
||
|
||
// Ровно тот баг, который это всё затевалось починить: подвкладка «Инициированные»
|
||
// с выбранным фильтром → уход на другую подвкладку → возврат — фильтр должен
|
||
// остаться прежним, а не сброситься на «Все статусы» (см. RequestPortal.tsx/
|
||
// gotoOwnTab — persistOwnTab НЕ трогает сохранённый статус).
|
||
it('переключение на другую подвкладку и обратно не стирает запомненный фильтр', () => {
|
||
persistOwnStatus('На согласовании');
|
||
expect(parseRoute('?rpnav=own').ownStatus).toBe('На согласовании');
|
||
|
||
persistOwnTab('approved');
|
||
persistOwnTab('created');
|
||
expect(parseRoute('?rpnav=own').ownStatus).toBe('На согласовании');
|
||
});
|
||
|
||
it('localStorage недоступен (например, приватный режим) — не роняет parseRoute', () => {
|
||
const original = window.localStorage.getItem;
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- имитация приватного режима, где чтение localStorage бросает
|
||
(window.localStorage as any).getItem = (): never => {
|
||
throw new Error('SecurityError');
|
||
};
|
||
try {
|
||
expect(() => parseRoute('?rpnav=own')).not.toThrow();
|
||
expect(parseRoute('?rpnav=own').ownTab).toBe('created');
|
||
} finally {
|
||
window.localStorage.getItem = original;
|
||
}
|
||
});
|
||
|
||
it('round-trip для подвкладки решений', () => {
|
||
navigate({ mode: 'submit', nav: 'own', ownTab: 'executed' });
|
||
const route = parseRoute(window.location.search);
|
||
expect(route.nav).toBe('own');
|
||
expect(route.ownTab).toBe('executed');
|
||
});
|
||
});
|
||
|
||
describe('routeUrl', () => {
|
||
it('раздел по умолчанию не засоряет адрес параметром', () => {
|
||
expect(routeUrl({ mode: 'submit', nav: 'new' })).toBe('/');
|
||
});
|
||
|
||
it('непустой раздел попадает в адрес', () => {
|
||
expect(routeUrl({ mode: 'submit', nav: 'own' })).toContain('rpnav=own');
|
||
});
|
||
|
||
it('сохраняет посторонние параметры страницы (их ставит SharePoint)', () => {
|
||
window.history.replaceState({}, '', '/page.aspx?env=WebViewList');
|
||
const url = routeUrl({ mode: 'submit', nav: 'own' });
|
||
expect(url).toContain('env=WebViewList');
|
||
expect(url).toContain('rpnav=own');
|
||
});
|
||
|
||
it('заявка без formKey/itemId не добавляет параметров открытия', () => {
|
||
const url = routeUrl({ mode: 'approve', nav: 'new' } as IRoute);
|
||
expect(url).not.toContain('rpmode');
|
||
});
|
||
});
|
||
|
||
describe('navigate', () => {
|
||
it('после перехода маршрут читается обратно тем же parseRoute (round-trip)', () => {
|
||
navigate({ mode: 'approve', nav: 'own', formKey: 'hire', itemId: 42 });
|
||
expect(parseRoute(window.location.search)).toEqual({
|
||
mode: 'approve',
|
||
nav: 'own',
|
||
formKey: 'hire',
|
||
itemId: 42,
|
||
// Подвкладка «Моих заявок» всегда определена внутри раздела — в адресе её
|
||
// нет только потому, что это значение по умолчанию.
|
||
ownTab: 'created',
|
||
ownStatus: undefined,
|
||
page: 1,
|
||
query: '',
|
||
});
|
||
});
|
||
|
||
it('подвкладка и фильтр «Моих заявок» переживают открытие и закрытие заявки', () => {
|
||
navigate({ mode: 'submit', nav: 'own', ownTab: 'created', ownStatus: 'Черновик' });
|
||
navigate({ mode: 'approve', nav: 'own', formKey: 'hire', itemId: 42, ownTab: 'created', ownStatus: 'Черновик' });
|
||
|
||
const opened = parseRoute(window.location.search);
|
||
expect(opened.ownStatus).toBe('Черновик');
|
||
|
||
navigate({ mode: 'submit', nav: 'own', ownTab: opened.ownTab, ownStatus: opened.ownStatus });
|
||
expect(parseRoute(window.location.search).ownStatus).toBe('Черновик');
|
||
});
|
||
|
||
it('round-trip для создания производной заявки', () => {
|
||
navigate({ mode: 'create', nav: 'new', formKey: 'workplace', sourceId: 15 });
|
||
expect(parseRoute(window.location.search)).toEqual({
|
||
mode: 'create',
|
||
nav: 'new',
|
||
formKey: 'workplace',
|
||
sourceId: 15,
|
||
});
|
||
});
|
||
|
||
it('возврат к подаче заявки убирает параметры открытой заявки из адреса', () => {
|
||
navigate({ mode: 'approve', nav: 'new', formKey: 'hire', itemId: 42 });
|
||
navigate({ mode: 'submit', nav: 'new' });
|
||
expect(window.location.search).toBe('');
|
||
});
|
||
|
||
it('добавляет запись в историю (pushState, а не replaceState)', () => {
|
||
const before = window.history.length;
|
||
navigate({ mode: 'submit', nav: 'own' });
|
||
expect(window.history.length).toBe(before + 1);
|
||
});
|
||
|
||
it('оповещает подписчиков своим событием (pushState сам popstate не порождает)', () => {
|
||
const seen: string[] = [];
|
||
const onChange = (): void => {
|
||
seen.push(window.location.search);
|
||
};
|
||
window.addEventListener('rp-route-change', onChange);
|
||
|
||
navigate({ mode: 'submit', nav: 'approvals' });
|
||
|
||
window.removeEventListener('rp-route-change', onChange);
|
||
expect(seen).toHaveLength(1);
|
||
expect(seen[0]).toContain('rpnav=approvals');
|
||
});
|
||
});
|
||
|
||
describe('requestLinkUrl / navigateToRequestLink', () => {
|
||
it('ссылка абсолютна и не тащит посторонние параметры текущей страницы', () => {
|
||
window.history.replaceState({}, '', '/?foo=bar&rpnav=own');
|
||
expect(requestLinkUrl('clearance-form', 7, '/sites/requests/Lists/List10')).toBe(
|
||
'http://localhost/?rpmode=approve&rptype=clearance-form&rpid=7&rplist=%2Fsites%2Frequests%2FLists%2FList10'
|
||
);
|
||
});
|
||
|
||
it('ссылка на заявку этого же портала открывается навигацией, без перезагрузки', () => {
|
||
window.history.replaceState({}, '', '/?rpnav=own');
|
||
expect(navigateToRequestLink(requestLinkUrl('clearance-form', 7))).toBe(true);
|
||
|
||
const route = parseRoute();
|
||
expect(route.mode).toBe('approve');
|
||
expect(route.formKey).toBe('clearance-form');
|
||
expect(route.itemId).toBe(7);
|
||
// Раздел под панелью заявки остаётся тем же, что был открыт.
|
||
expect(route.nav).toBe('own');
|
||
});
|
||
|
||
it('чужой сайт, другая страница и ссылка не на заявку — клик отдаётся браузеру', () => {
|
||
expect(navigateToRequestLink('https://example.com/p?rpmode=approve&rptype=a&rpid=1')).toBe(false);
|
||
expect(navigateToRequestLink('/OtherPage.aspx?rpmode=approve&rptype=a&rpid=1')).toBe(false);
|
||
expect(navigateToRequestLink('http://localhost/?rpnav=own')).toBe(false);
|
||
});
|
||
|
||
it('мусор вместо адреса не роняет обработчик клика', () => {
|
||
expect(navigateToRequestLink('не ссылка')).toBe(false);
|
||
});
|
||
});
|