dana-medical-site/web/scripts/test-menu-switch.mjs
2026-10-22 12:44:21 +00:00

93 lines
5.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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('<html><body><div id="root"></div></body></html>');
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();
}