From f9f45e5eaec59979cc028b3222f397f9aa559b0c Mon Sep 17 00:00:00 2001 From: Dana Medical Developer Date: Thu, 22 Oct 2026 14:34:30 +0000 Subject: [PATCH] fix(checkup): request programs without selecting a doctor --- web/scripts/test-checkup-request.mjs | 90 ++++++++++++++++++++++++++++ web/src/components/LeadModal.jsx | 19 +++++- web/src/pages/CheckupPage.jsx | 2 +- web/src/pages/PromosPage.jsx | 7 ++- 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 web/scripts/test-checkup-request.mjs diff --git a/web/scripts/test-checkup-request.mjs b/web/scripts/test-checkup-request.mjs new file mode 100644 index 0000000..10193fb --- /dev/null +++ b/web/scripts/test-checkup-request.mjs @@ -0,0 +1,90 @@ +// Fictional contact data and mocked submissions only. No external requests. +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://test.invalid/sales/'); +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, Routes, Route } = await import('react-router-dom'); +const vite = await createServer({ server: { middlewareMode: true, hmr: false }, appType: 'custom' }); +const { api } = await vite.ssrLoadModule('/src/api.js'); +const { LeadModalProvider } = await vite.ssrLoadModule('/src/context/LeadModalContext.jsx'); +const { default: PromosPage } = await vite.ssrLoadModule('/src/pages/PromosPage.jsx'); +const { default: CheckupPage } = await vite.ssrLoadModule('/src/pages/CheckupPage.jsx'); +const program = { id: 1, slug: 'chek-ap-ya-v-sad', name: 'Я в сад', audience: 'kids', blocks: [], seo: {} }; +api.promos = async () => ({ data: [ + { id: 1, slug: 'ya-v-sad-sale', title: 'Я в сад' }, + { id: 2, slug: 'unrelated-promo', title: 'Другая акция', checkup_slug: program.slug }, +] }); +api.checkups = async () => ({ data: [program] }); +api.checkup = async () => ({ data: program }); +let bookingCalls = 0; +api.doctors = api.bookingAvailability = api.doctorSlots = async () => { + bookingCalls++; + throw new Error('Checkup request must not load doctors or schedules'); +}; +const submissions = []; +api.createLead = async (payload) => { submissions.push(payload); return { data: { message: 'Тестовая заявка принята' } }; }; +api.createAppointment = () => { throw new Error('Checkup must not create an appointment in 1C'); }; +const container = document.getElementById('root'); +const root = createRoot(container); +const click = async (node) => { + assert.ok(node, 'Expected actionable element'); + await act(async () => node.dispatchEvent(new window.Event('click', { bubbles: true }))); +}; +const mount = async (url, Page, route) => act(async () => root.render( + React.createElement(MemoryRouter, { key: url, initialEntries: [url] }, + React.createElement(LeadModalProvider, {}, React.createElement(Routes, {}, + React.createElement(Route, { path: route, element: React.createElement(Page) })))))); +function checkSimpleForm() { + const modal = container.querySelector('[role="dialog"]'); + assert.ok(modal); + assert.match(modal.textContent, /Записаться на чек-ап/); + assert.match(modal.textContent, /Программа: Я в сад/); + assert.equal(modal.querySelectorAll('input').length, 3, 'Only name, phone and consent'); + assert.equal(modal.querySelector('select, textarea, input[type="date"], .doctor-picker, .native-booking'), null); + assert.equal(bookingCalls, 0); + return modal; +} +try { + await mount('/sales/', PromosPage, '/sales/'); + for (const card of container.querySelectorAll('.promo-card')) { + await click(card.querySelector('button')); + checkSimpleForm(); + await click(container.querySelector('[aria-label="Закрыть"]')); + } + await click(container.querySelector('.promo-card button')); + let modal = checkSimpleForm(); + await act(async () => Simulate.submit(modal.querySelector('form'))); + assert.equal(submissions.length, 0, 'Empty form rejected'); + await act(async () => Simulate.change(modal.querySelector('input[type="text"]'), { target: { value: 'Тестовая заявка' } })); + await act(async () => Simulate.change(modal.querySelector('input[type="tel"]'), { target: { value: '79990000000' } })); + await act(async () => Simulate.submit(modal.querySelector('form'))); + assert.equal(submissions.length, 0, 'Consent required'); + await act(async () => Simulate.change(modal.querySelector('input[type="checkbox"]'), { target: { type: 'checkbox', checked: true } })); + await act(async () => Simulate.submit(modal.querySelector('form'))); + assert.equal(submissions.length, 1); + assert.equal(submissions[0].form, 'appointment', 'Use existing lead endpoint form'); + assert.deepEqual(submissions[0].extra, { checkup_slug: program.slug, checkup_name: program.name }); + assert.match(submissions[0].message, /Чек-ап: Я в сад/); + assert.match(submissions[0].message, /Акция: Я в сад/); + assert.equal(submissions[0].doctor_id, undefined); + assert.equal(submissions[0].slot, undefined); + assert.equal(submissions[0].consent, true); + assert.match(container.querySelector('[role="dialog"]').textContent, /Тестовая заявка принята/); + await mount(`/checkup/${program.slug}/`, CheckupPage, '/checkup/:slug/'); + await click(container.querySelector('.checkup-hero__btn')); + checkSimpleForm(); + await click(container.querySelector('[aria-label="Закрыть"]')); + const bottomCta = [...container.querySelectorAll('button')].find((node) => node.textContent === 'Оставить заявку'); + await click(bottomCta); + checkSimpleForm(); + console.log('PASS: promotion links (slug fallback and explicit), checkup hero/bottom CTAs, contact validation and program metadata; no doctors, schedules or 1C appointment calls'); +} finally { + await act(async () => root.unmount()); + await vite.close(); +} diff --git a/web/src/components/LeadModal.jsx b/web/src/components/LeadModal.jsx index bf86cfc..420c0c7 100644 --- a/web/src/components/LeadModal.jsx +++ b/web/src/components/LeadModal.jsx @@ -14,6 +14,10 @@ import { todayKey } from '../lib/slots.js'; import './LeadModal.css'; const COPY = { + checkup: { + title: 'Записаться на чек-ап', + subtitle: 'Оставьте контакты — администратор согласует дату и расскажет о подготовке', + }, appointment: { title: 'Записаться на приём', subtitle: 'Выберите дату — покажем врачей со свободными окнами на этот день', @@ -32,7 +36,7 @@ const COPY = { }, }; -export default function LeadModal({ form = 'appointment', doctor, slot, service, specialty, message = '', onClose }) { +export default function LeadModal({ form = 'appointment', doctor, slot, service, specialty, program, message = '', onClose }) { const copy = COPY[form] || COPY.appointment; const { hours } = useSiteData(); const uid = useId(); @@ -202,11 +206,17 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service, const extra = {}; if (form === 'review') extra.rating = values.rating; + if (form === 'checkup' && program) { + extra.checkup_slug = program.slug; + extra.checkup_name = program.name; + } setStatus({ state: 'sending', message: '' }); try { const { data } = await api.createLead({ - form, + // Заявка на программу использует существующую форму записи, + // а не создание приёма конкретного врача в 1С. + form: form === 'checkup' ? 'appointment' : form, name: values.name.trim(), phone: values.phone, doctor_id: values.doctorId ? Number(values.doctorId) : undefined, @@ -298,6 +308,9 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service, Свободные окна этого врача сейчас недоступны для онлайн-записи. Оставьте контакты — администратор уточнит расписание.

)} + {form === 'checkup' && program?.name && ( +

Программа: {program.name}

+ )}
)} - {form !== 'callback' && ( + {form !== 'callback' && form !== 'checkup' && (