// Regression for cross-menu data leaks. All catalog responses are fictional; // no network calls or patient data are used. import assert from 'node:assert/strict'; import { parseHTML } from 'linkedom'; import { createServer } from 'vite'; const { window } = parseHTML('
'); Object.assign(globalThis, { window, document: window.document, HTMLElement: window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true }); window.location = new URL('https://menu-test.invalid/'); document.oninput = null; const { default: React, act } = await import('react'); const { createRoot } = await import('react-dom/client'); const { Simulate } = await import('react-dom/test-utils'); const { MemoryRouter } = await import('react-router-dom'); const vite = await createServer({ server: { middlewareMode: true, hmr: false }, appType: 'custom' }); const { default: Header } = await vite.ssrLoadModule('/src/components/Header.jsx'); const { api } = await vite.ssrLoadModule('/src/api.js'); const { ApiDataProvider } = await vite.ssrLoadModule('/src/context/ApiData.jsx'); const pending = []; for (const [method, kind] of [['services', 'services'], ['specialties', 'doctors'], ['priceSections', 'prices']]) { api[method] = ({ signal }) => new Promise((resolve, reject) => pending.push({ kind, signal, resolve, reject })); } const container = document.getElementById('root'); const root = createRoot(container); const prices = [{ id: 1, name: 'Тестовая консультация', slug: 'test-price', count: 411 }]; const specialties = [{ id: 2, name: 'Тестовые педиатры', slug: 'test-kids', count: 3 }]; const services = [{ id: 3, name: 'Тестовая услуга', path: 'test-service', children: [] }]; const panel = () => container.querySelector('.mega'); const panelText = () => panel()?.textContent || ''; const links = () => [...(panel()?.querySelectorAll('.mega__item') || [])].map((link) => link.getAttribute('href')); const trigger = (title) => [...container.querySelectorAll('.nav__item')].find((node) => node.textContent === title); async function open(title, focus = false) { await act(async () => Simulate[focus ? 'focus' : 'mouseEnter'](trigger(title))); return pending.at(-1); } async function reply(request, data) { await act(async () => request.resolve({ data })); } async function render(initial = {}) { await act(async () => root.render(React.createElement(MemoryRouter, {}, React.createElement(ApiDataProvider, { value: initial }, React.createElement(Header))))); } try { await render(); const priceRequest = await open('Цены'); await reply(priceRequest, prices); assert.match(panelText(), /411 услуг/); const doctorRequest = await open('Врачи'); assert.equal(panel().getAttribute('aria-label'), 'Меню «Врачи»'); assert.deepEqual(links(), [], 'Price sections must disappear immediately when switching to doctors'); assert.match(panelText(), /Загружаем/); assert.equal(priceRequest.signal.aborted, true, 'Previous menu request is cancelled on switch'); await reply(doctorRequest, specialties); assert.match(panelText(), /3 врача/); assert.deepEqual(links(), ['/company/staff/test-kids/']); assert.doesNotMatch(panelText(), /411|Тестовая консультация/); const delayedPrice = await open('Цены'); assert.deepEqual(links(), [], 'Doctor specialties must not leak into the price menu'); const delayedServices = await open('Услуги'); const latestDoctor = await open('Врачи', true); assert.equal(delayedPrice.signal.aborted, true); assert.equal(delayedServices.signal.aborted, true); await reply(latestDoctor, specialties); // Deliberately resolve cancelled requests: even a transport ignoring AbortSignal // must not overwrite a newly opened menu. await reply(delayedPrice, prices); await reply(delayedServices, services); assert.deepEqual(links(), ['/company/staff/test-kids/']); assert.match(panelText(), /3 врача/); assert.doesNotMatch(panelText(), /411|Тестовая консультация|Тестовая услуга/); const serviceRequest = await open('Услуги'); await reply(serviceRequest, services); assert.deepEqual(links(), ['/services/test-service/']); const failedPrice = await open('Цены'); await act(async () => failedPrice.reject(new Error('Fictional failure'))); assert.match(panelText(), /Не удалось загрузить список/); assert.deepEqual(links(), [], 'An error must not leave links from another menu'); await act(async () => root.render(null)); await render({ specialties: { data: specialties, meta: {} } }); const initialPrices = await open('Цены'); await reply(initialPrices, prices); const initialDoctors = await open('Врачи', true); assert.deepEqual(links(), ['/company/staff/test-kids/'], 'SSR data belongs only to its own catalog'); assert.match(panelText(), /3 врача/); await reply(initialDoctors, specialties); console.log('PASS: hover/focus menu switching, delayed replies, cancellation, errors, and SSR data isolation'); } finally { await act(async () => root.unmount()); await vite.close(); }