122 lines
7.2 KiB
JavaScript
122 lines
7.2 KiB
JavaScript
// Изолированные UI-регрессии: только вымышленные врачи и окна, без сети и заявок.
|
||
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://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();
|
||
}
|