diff --git a/public/core/ux-fixes-v1764.js b/public/core/ux-fixes-v1764.js index 5b71ad3..cd44b41 100644 --- a/public/core/ux-fixes-v1764.js +++ b/public/core/ux-fixes-v1764.js @@ -176,7 +176,18 @@ if(!box){box=document.createElement('section');box.id='sunOfferTemplateV1764';box.className='sun-v1764-offer-template';const head=dialog.querySelector('.sun-offer-modal-head');if(head)head.insertAdjacentElement('afterend',box);else dialog.prepend(box);box.addEventListener('click',e=>{const b=e.target.closest('[data-v1764-offer-template]');if(!b)return;const id=b.dataset.v1764OfferTemplate;if(!persistOfferTemplate(activeOfferOrderId,id))return;renderOfferPicker();toast(`Оформление «${templateList().find(x=>x.id===id)?.name||id}» сохранено для этого предложения.`,'success',3200)});} renderOfferPicker();return true; } - function renderOfferPicker(){const box=$('sunOfferTemplateV1764');if(box)box.innerHTML=pickerHtml()} + function offerPickerSignature(){ + const list=templateList(),current=activeTemplateId(); + return JSON.stringify({current,templates:list.map(t=>[String(t?.id||''),String(t?.name||'')])}); + } + function renderOfferPicker({force=false}={}){ + const box=$('sunOfferTemplateV1764');if(!box)return false; + const signature=offerPickerSignature(); + if(!force&&box.dataset.sunV1764Signature===signature)return false; + box.innerHTML=pickerHtml(); + box.dataset.sunV1764Signature=signature; + return true; + } function setActiveOffer(id){if(id==null||id==='')return;activeOfferOrderId=String(id);setTimeout(ensureOfferPicker,0)} function clearActiveOffer(){activeOfferOrderId=null} function patchOfferOpenExports(){ diff --git a/tests/ui-stability.spec.mjs b/tests/ui-stability.spec.mjs index 7a5dc84..42550c9 100644 --- a/tests/ui-stability.spec.mjs +++ b/tests/ui-stability.spec.mjs @@ -118,6 +118,46 @@ test('a delayed menu editor cannot move back into a section that was already lef await expect(page.locator('#editor')).not.toHaveClass(/\bon\b/); }); + +test('offer template picker is not rebuilt by the five-second maintenance timer',async({page})=>{ + await fixture(page,''); + await page.evaluate(()=>{ + localStorage.setItem('sunOrders',JSON.stringify([{id:'one',clientOfferTemplateId:'light'}])); + window.SunOfferTemplate={ + get:()=>({id:'light',template:{id:'light',name:'Светлый'}}), + list:()=>[{id:'light',name:'Светлый'},{id:'editorial-grid',name:'Редакционный'}] + }; + window.CateriumProposalPDF={resolveTemplate:id=>id,IDS:[]}; + window.sunOpenClientOffer=()=>{}; + window.sunOpenCurrentClientOffer=()=>{}; + }); + await page.addScriptTag({url:'/core/ux-fixes-v1764.js'}); + await page.locator('#offerOpen').click(); + const picker=page.locator('#sunOfferTemplateV1764'); + await expect(picker).toBeVisible(); + await expect(picker.locator('[data-v1764-offer-template]')).toHaveCount(2); + const before=await picker.evaluate(el=>({ + first:el.querySelector('[data-v1764-offer-template]'), + html:el.innerHTML, + signature:el.dataset.sunV1764Signature + })); + await page.evaluate(()=>{ + const picker=document.getElementById('sunOfferTemplateV1764'); + window.offerPickerMutations=0; + new MutationObserver(records=>{window.offerPickerMutations+=records.filter(r=>r.type==='childList').length}).observe(picker,{childList:true,subtree:true}); + }); + await page.waitForTimeout(5300); + expect(await page.evaluate(()=>window.offerPickerMutations)).toBe(0); + expect(await picker.evaluate((el,before)=>el.innerHTML===before.html&&el.dataset.sunV1764Signature===before.signature,before)).toBe(true); + await picker.locator('[data-v1764-offer-template="editorial-grid"]').click(); + await expect(picker.locator('[data-v1764-offer-template="editorial-grid"]')).toHaveClass(/\bon\b/); + const afterSelection=await page.evaluate(()=>window.offerPickerMutations); + expect(afterSelection).toBeGreaterThan(0); + await page.evaluate(()=>window.offerPickerMutations=0); + await page.waitForTimeout(5300); + expect(await page.evaluate(()=>window.offerPickerMutations)).toBe(0); +}); + test('full app stays stable across settings, menu, calendar and delayed background refreshes',async({page})=>{ const errors=[];page.on('pageerror',e=>errors.push(String(e))); const workspaceId='22222222-2222-4222-8222-222222222222',servedRpcs=new Set();