/** The live UI and real tests import this same module. Broken branches are teaching fixtures. */ export function createLab(variant = 'fixed') { if (!['fixed', 'broken'].includes(variant)) throw new Error('Unknown implementation'); const broken = variant === 'broken'; return { total(priceCents, quantity, discountPercent = 0) { if (!Number.isSafeInteger(priceCents) || priceCents < 0 || !Number.isInteger(quantity) || quantity < 1 || quantity > 100 || !Number.isSafeInteger(priceCents * quantity) || !Number.isFinite(discountPercent) || discountPercent < 0 || discountPercent > 100) throw new RangeError('Некорректная цена, количество или скидка'); if (broken) return Math.round(priceCents * quantity * (1 - discountPercent / 100)) - 1; return Math.round(priceCents * quantity * (1 - discountPercent / 100)); }, validateEmail(value) { if (typeof value !== 'string') return false; const email = broken ? value : value.trim(); return broken ? email.includes('@') : /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }, async loadCustomer(fetcher, url, signal) { const response = await fetcher(url, { signal }); if (!broken && !response.ok) throw new Error(`HTTP ${response.status}`); const customer = await response.json(); if (!broken && (typeof customer.name !== 'string' || typeof customer.id !== 'number')) throw new TypeError('Некорректный клиент'); return customer; }, async saveOrder(client, email, cents) { if (!this.validateEmail(email)) throw new TypeError('Укажите корректный email'); return client.post('/orders', { email: broken ? email : email.trim(), totalCents: broken ? cents / 100 : cents }); }, debounceSearch(callback, delay = 300) { let timer = null; const pending = new Set(); const run = (query) => { if (!broken && timer !== null) { clearTimeout(timer); pending.delete(timer); } const id = setTimeout(() => { pending.delete(id); callback(query); }, delay); timer = id; pending.add(id); }; run.cancel = () => { for (const id of pending) clearTimeout(id); pending.clear(); timer = null; }; return run; }, mountContactForm(root) { root.innerHTML = '

'; const form = root.querySelector('form'), input = root.querySelector('input'), status = root.querySelector('[role=status]'); const submit = (event) => { event.preventDefault(); const valid = this.validateEmail(input.value); if (!valid && !broken) { status.textContent = 'Укажите корректный email'; input.setAttribute('aria-invalid', 'true'); input.focus(); return; } input.removeAttribute('aria-invalid'); status.textContent = 'Сохранено: ' + input.value.trim(); }; form.addEventListener('submit', submit); return () => { form.removeEventListener('submit', submit); root.replaceChildren(); }; }, mountDisclosure(root) { root.innerHTML = '
'; const toggle = root.querySelector('#details-toggle'), panel = root.querySelector('#details-panel'), input = root.querySelector('input'), close = root.querySelector('#details-close'); const openPanel = () => { panel.hidden = !panel.hidden; if (!broken) toggle.setAttribute('aria-expanded', String(!panel.hidden)); if (!panel.hidden) input.focus(); }; const closePanel = () => { panel.hidden = true; if (!broken) { toggle.setAttribute('aria-expanded', 'false'); toggle.focus(); } }; const key = e => { if(e.key === 'Escape' && !panel.hidden) closePanel(); }; toggle.addEventListener('click', openPanel); close.addEventListener('click', closePanel); root.addEventListener('keydown', key); return () => { toggle.removeEventListener('click', openPanel); close.removeEventListener('click', closePanel); root.removeEventListener('keydown', key); root.replaceChildren(); }; }, summary(order) { return { id: order.id, customer: order.customer, amount: (order.cents / 100).toFixed(2), currency: broken ? 'USD' : 'RUB', state: order.paid ? 'paid' : 'pending' }; }, canPublish(checks) { if (!Array.isArray(checks) || checks.length === 0) return false; return broken ? checks.some(Boolean) : checks.every(Boolean); }, // Deliberately only partly covered in the small course: percentages are not proof of requirements. deliveryFee(zone) { if(zone === 'pickup') return 0; if(zone === 'city') return 500; if(zone === 'regional') return 1500; throw new RangeError('Unknown delivery zone'); } }; }