fix(checkup): request programs without selecting a doctor
This commit is contained in:
parent
d791ea4d12
commit
f9f45e5eae
90
web/scripts/test-checkup-request.mjs
Normal file
90
web/scripts/test-checkup-request.mjs
Normal file
@ -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('<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://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();
|
||||
}
|
||||
@ -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,
|
||||
Свободные окна этого врача сейчас недоступны для онлайн-записи. Оставьте контакты — администратор уточнит расписание.
|
||||
</p>
|
||||
)}
|
||||
{form === 'checkup' && program?.name && (
|
||||
<p className="lead-modal__availability">Программа: <strong>{program.name}</strong></p>
|
||||
)}
|
||||
<div className="lead-modal__row lead-modal__contacts">
|
||||
<label className="lead-field">
|
||||
<span className="lead-field__label">Ваше имя</span>
|
||||
@ -369,7 +382,7 @@ export default function LeadModal({ form = 'appointment', doctor, slot, service,
|
||||
</div>
|
||||
)}
|
||||
|
||||
{form !== 'callback' && (
|
||||
{form !== 'callback' && form !== 'checkup' && (
|
||||
<label className="lead-field lead-field--comment">
|
||||
<span className="lead-field__label">{form === 'review' ? 'Текст отзыва' : 'Комментарий'}</span>
|
||||
<textarea
|
||||
|
||||
@ -32,7 +32,7 @@ export default function CheckupPage() {
|
||||
const intro = introText(c.blocks);
|
||||
const lead = cleanText(c.lead);
|
||||
|
||||
const book = () => open({ form: 'appointment', message: `Чек-ап: ${name}` });
|
||||
const book = () => open({ form: 'checkup', program: { name, slug: c.slug }, message: `Чек-ап: ${name}` });
|
||||
const askPrice = () => open({ form: 'service_order', message: `Стоимость чек-апа: ${name}` });
|
||||
|
||||
// первый столбец списка заполняется сверху вниз, затем второй — как в макете
|
||||
|
||||
@ -47,6 +47,7 @@ export default function PromosPage() {
|
||||
{(promos.data || []).map((p) => {
|
||||
// связь с чек-апом задаётся в админке; сопоставление по адресу — запасной вариант
|
||||
const checkup = p.checkup_slug ? { slug: p.checkup_slug } : checkupBySlug.get(baseSlug(p.slug));
|
||||
const programName = checkup ? cleanText(checkupBySlug.get(baseSlug(checkup.slug))?.name || p.title) : '';
|
||||
const image = promoImage(p);
|
||||
const lead = cleanText(p.lead);
|
||||
return (
|
||||
@ -71,7 +72,11 @@ export default function PromosPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--coral promo-card__btn"
|
||||
onClick={() => open({ form: 'appointment', message: `Акция: ${p.title}` })}
|
||||
onClick={() => open(checkup ? {
|
||||
form: 'checkup',
|
||||
program: { slug: checkup.slug, name: programName },
|
||||
message: `Чек-ап: ${programName}. Акция: ${cleanText(p.title)}`,
|
||||
} : { form: 'appointment', message: `Акция: ${p.title}` })}
|
||||
>
|
||||
Записаться
|
||||
</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user