// Изолированные UI-регрессии: только вымышленные врачи и окна, без сети и заявок. 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://booking-test.invalid/'); 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: LeadModal } = await vite.ssrLoadModule('/src/components/LeadModal.jsx'); const { api } = await vite.ssrLoadModule('/src/api.js'); const doctors = [ { id: 1, slug: 'first', full_name: 'Первый врач', booking: { provider: 'native' } }, { id: 2, slug: 'second', full_name: 'Второй врач', booking: { provider: 'native' } }, { id: 3, slug: 'fallback', full_name: 'Врач без расписания', booking: { provider: 'none' } }, ]; const pending = []; api.doctors = async () => ({ data: doctors }); api.bookingAvailability = async () => ({ data: { enabled: true, days: [ { date: '2070-10-23', doctor_ids: [1, 2] }, { date: '2070-10-24', doctor_ids: [2] }, ] } }); api.doctorSlots = (slug) => new Promise((resolve, reject) => pending.push({ slug, resolve, reject })); api.createLead = api.createAppointment = () => { throw new Error('Real submissions are forbidden in this test'); }; const container = document.getElementById('root'); const root = createRoot(container); const render = async (props = {}) => act(async () => root.render(React.createElement(MemoryRouter, {}, React.createElement(LeadModal, { ...props, onClose() {} })))); const text = () => container.textContent; const times = () => [...container.querySelectorAll('.slot-chip--time')].map((e) => e.textContent); async function chooseDoctor(id) { const select = container.querySelector('select'); await act(async () => { // linkedom exposes select.value as read-only; option.selected sets the value. for (const option of select.options) option.selected = false; [...select.options].find((option) => option.value === String(id)).selected = true; select.dispatchEvent(new window.Event('change', { bubbles: true })); }); } const free = (time) => ({ start: `2070-10-23T${time}:00+03:00`, status: 'free', branch_id: 'test', branch: 'Тестовый филиал' }); async function reply(request, slots, enabled = true) { await act(async () => request.resolve({ data: { enabled, slots, services: [] } })); } try { await render(); assert.equal(pending.length, 0, 'No schedule requests before selecting a doctor'); assert.ok(container.querySelector('input[type="date"]')); assert.equal(container.querySelector('input[type="tel"]'), null); assert.deepEqual(times(), []); assert.match(text(), /Выберите дату или врача/); await chooseDoctor(1); assert.equal(pending[0].slug, 'first'); assert.match(text(), /Загружаем свободное время/); await reply(pending[0], [free('09:15'), { ...free('10:00'), status: 'busy' }, { ...free('11:00'), start: '2000-01-01T11:00:00+03:00' }, { ...free('12:00'), start: 'invalid' }]); assert.deepEqual(times(), ['09:15'], 'Only actual free future slots appear'); await act(async () => container.querySelector('.slot-chip--time').dispatchEvent(new window.Event('click', { bubbles: true }))); assert.ok(container.querySelector('input[type="tel"]'), 'Contacts follow an actual slot'); await chooseDoctor(2); assert.deepEqual(times(), []); assert.equal(container.querySelector('input[type="tel"]'), null, 'Switching doctor clears the chosen slot'); assert.equal(pending[1].slug, 'second'); await chooseDoctor(1); assert.equal(pending[2].slug, 'first'); await reply(pending[1], [], false); assert.match(text(), /Загружаем свободное время/, 'Late response for another doctor must be ignored'); await reply(pending[2], [free('15:45')]); assert.deepEqual(times(), ['15:45']); await chooseDoctor(''); assert.deepEqual(times(), []); assert.equal(pending.length, 3, 'Clearing selection must not request an all-doctors schedule'); await chooseDoctor(3); assert.match(text(), /Свободные окна этого врача сейчас недоступны/); assert.deepEqual(times(), []); assert.equal(container.querySelectorAll('.slot-chip--time').length, 0, 'Fallback cannot offer invented times'); assert.ok(container.querySelector('input[type="tel"]')); await chooseDoctor(2); await act(async () => pending[3].reject(new Error('Schedule temporarily unavailable'))); assert.match(text(), /Свободные окна этого врача сейчас недоступны/); assert.deepEqual(times(), []); await act(async () => root.render(null)); await render({ doctor: doctors[0], slot: free('10:00') }); await reply(pending[4], [free('16:15')]); assert.deepEqual(times(), ['16:15']); assert.equal(container.querySelector('input[type="tel"]'), null, 'A stale initial slot must not be preselected'); await act(async () => root.render(null)); await render(); await act(async () => Simulate.input(container.querySelector('input[type="date"]'), { target: { value: '2070-10-24' } })); assert.deepEqual([...container.querySelector('select').options].map((option) => option.value), ['', '2'], 'Only doctors free on the selected date appear'); await chooseDoctor(2); await reply(pending.at(-1), [free('09:00'), { ...free('14:15'), start: '2070-10-24T14:15:00+03:00' }]); assert.deepEqual(times(), ['14:15'], 'Selected date restricts the actual time list too'); await act(async () => Simulate.change(container.querySelector('input[type="date"]'), { target: { value: '2070-10-25' } })); assert.deepEqual(times(), []); assert.equal(container.querySelector('select').value, '', 'Changing date clears the selected doctor and window'); assert.equal(container.querySelector('select').options.length, 1); assert.match(text(), /На эту дату свободных окон нет/); await act(async () => Simulate.change(container.querySelector('input[type="date"]'), { target: { value: '' } })); assert.equal(container.querySelector('select').options.length, 4, 'Clearing date restores the doctor-first flow'); await act(async () => root.render(null)); await render({ form: 'callback' }); assert.ok(container.querySelector('input[type="tel"]')); assert.equal(container.querySelector('select'), null); await act(async () => root.render(null)); await render({ form: 'review' }); assert.equal(container.querySelectorAll('.lead-rating__star').length, 5); assert.ok(container.querySelector('select')); console.log('PASS: date-to-doctor filtering, actual times on selected date, reset/empty dates, doctor-first flow, stale responses, fallback, callback and review'); } finally { await act(async () => root.unmount()); await vite.close(); }