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); }); });