diff --git a/docs/release-manifest.json b/docs/release-manifest.json index d5112ad..5210d98 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v109-20260918-mobile-calendar", + "pwaCache": "v110-20260918-ui-stability", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, @@ -323,7 +323,7 @@ "calendarOverflowPanel": true, "routeBaseConfigurable": true, "routeOrderPopup": true, - "developerGateHotfixV1763": true, + "developerGateHotfixV1763": false, "saasClickHotfixV1763": true, "ordersAutoCompleteDelayMs": 60000, "ordersAutoPayment": true, @@ -433,5 +433,8 @@ "proposalLogoAspectRatio": true, "proposalLogoContrastMasthead": true, "retiredCatalogItemsPreserveOrderHistory": true, - "oneTimeTelegramArchiveUiRemoved": true + "oneTimeTelegramArchiveUiRemoved": true, + "settingsIdleMutationLoopRemoved": true, + "subscriptionResponseTenantIsolation": true, + "stableBackgroundUiUpdates": true } diff --git a/docs/ui-stability-20260918.md b/docs/ui-stability-20260918.md new file mode 100644 index 0000000..09eda73 --- /dev/null +++ b/docs/ui-stability-20260918.md @@ -0,0 +1,18 @@ +# UI stability audit — 18 September 2026 + +## Confirmed defects and fixes + +- Settings refreshed their own DOM through a MutationObserver every 20 ms. An idle fixture produced 34 child-list changes in 350 ms. Catalog summaries and tab captions now update only when their content changes. New cards receive their tab visibility before the next paint. +- Settings card ordering repeatedly moved the same nodes. Ordering now walks backwards from its anchor and leaves already ordered cards in place. +- Unchanged cloud notifications replaced theme controls and removed input focus. Theme state is compared before applying it; viewport resizing uses existing responsive CSS without replaying the theme. +- The obsolete developer hotfix replaced the unified login while company access was still loading. Removed that duplicate renderer; retained safe opening of the developer console from its explicit button. +- A pending subscription request could apply the previous account's state and restore a blocking window. Requests are scoped to user and workspace; tenant changes invalidate requests and clear subscription windows. Repeated notifications share a request. Temporary failures retain the last confirmed subscription state. +- Subscription refresh rebuilt unchanged controls and overlays. Those nodes now persist. The plans dialog opens above the subscription blocker. +- A queued catalog editor callback could move the editor into an inactive section after navigation. It now checks the active section and skips duplicate docking. +- The common modal handler overwrote higher z-index values and could move focus after its window had closed. It now preserves stacking priority, checks the active window before autofocus, respects a user's newly focused input, and releases the page scroll lock when a window is removed. Plans also open above the dialog that requested them. + +## Regression coverage + +`tests/ui-stability.spec.mjs` covers idle DOM stability, focus preservation, real theme changes, account response races, dialog stacking, rapid menu navigation and a complete application session with mocked server responses. It runs in desktop Chromium, mobile Chromium and iPhone WebKit. Existing login, account isolation, theme startup and PDF tests remain in the release suite. + +No application data or database schema changes are needed for this release. Browser automation checks specific workflows; it does not establish the absence of every possible defect or substitute for testing on a physical iPhone. diff --git a/public/app-runtime.js b/public/app-runtime.js index 16e1b27..07ba176 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -4608,7 +4608,8 @@ window.SUN_LEGACY_CATALOG_V175=[]; let snapshot=null; let lastWorkspaceId=''; - let loading=false; + let loading=null; + let subscriptionScope=''; let guardedFns=new Map(); let adminRows=[]; @@ -4624,23 +4625,34 @@ window.SUN_LEGACY_CATALOG_V175=[]; function endDate(){return snapshot?.status==='trialing'?snapshot?.trial_ends_at:snapshot?.current_period_end;} function daysLeft(){const end=endDate();if(!end)return null;return Math.ceil((new Date(end).getTime()-Date.now())/86400000);} + const currentSubscriptionScope=()=>JSON.stringify([session()?.user?.id||'',workspace()?.id||'']); + function resetSubscription(){ + loading=null;snapshot=null;lastWorkspaceId='';subscriptionScope='';removeSubscriptionUi();applyReadOnlyControls(); + } async function refreshSnapshot(force=false){ const c=client(),ws=workspace(); - if(!c||!ws?.id){snapshot=null;lastWorkspaceId='';removeSubscriptionUi();return null;} - if(loading)return snapshot; + if(!c||!ws?.id){resetSubscription();return null;} + const scope=currentSubscriptionScope(); + if(subscriptionScope!==scope){resetSubscription();subscriptionScope=scope;} + if(loading)return loading.promise; if(!force&&lastWorkspaceId===ws.id&&snapshot)return snapshot; - loading=true; - try{ - const {data,error}=await c.rpc('sun_subscription_snapshot',{p_workspace:ws.id}); - if(error)throw error; - snapshot=Array.isArray(data)?(data[0]||null):data; - lastWorkspaceId=ws.id; - applyAll(); - window.dispatchEvent(new CustomEvent('sun:subscription-changed',{detail:snapshot})); - return snapshot; - }catch(err){console.error('[SaaS] subscription snapshot',err);snapshot=null;} - finally{loading=false;} - return snapshot; + const request={promise:null};loading=request; + const isCurrent=()=>loading===request&¤tSubscriptionScope()===scope; + request.promise=(async()=>{ + try{ + const {data,error}=await c.rpc('sun_subscription_snapshot',{p_workspace:ws.id}); + if(!isCurrent())return null; + if(error)throw error; + const next=Array.isArray(data)?(data[0]||null):data; + if(!next||!['full','read_only','blocked'].includes(next.access_mode))throw new Error('Некорректный ответ о подписке'); + snapshot=next;lastWorkspaceId=ws.id; + applyAll(); + window.dispatchEvent(new CustomEvent('sun:subscription-changed',{detail:snapshot})); + return snapshot; + }catch(err){if(isCurrent())console.error('[SaaS] subscription snapshot',err);return null;} + finally{if(loading===request)loading=null;} + })(); + return request.promise; } function installStyle(){ @@ -4655,7 +4667,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; .sun-saas-badge b{display:block;font-size:12px}.sun-saas-badge .warn{color:#ffd36b} .sun-saas-readonly-banner{position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:9800;background:#fff6df;border:1px solid #e2bd55;color:#6a5216;border-radius:12px;padding:9px 14px;box-shadow:0 8px 30px #0002;font-weight:800;max-width:min(680px,calc(100vw - 24px));text-align:center} .sun-saas-blocked{position:fixed;inset:0;z-index:12000;background:#122b3cf2;display:grid;place-items:center;padding:20px}.sun-saas-blocked-card{width:min(520px,100%);background:#fff;border-radius:18px;padding:24px;box-shadow:0 30px 90px #0007;text-align:center}.sun-saas-blocked-card img{width:72px;height:72px;object-fit:contain}.sun-saas-blocked-card h2{margin:8px 0;color:#17384d}.sun-saas-blocked-card p{color:#596773;line-height:1.5} - .sun-saas-modal{position:fixed;inset:0;z-index:11500;background:#0d2130c9;display:grid;place-items:center;padding:18px}.sun-saas-modal-card{width:min(900px,100%);max-height:92vh;overflow:auto;background:#fff;border-radius:18px;padding:20px;box-shadow:0 30px 90px #0007}.sun-saas-modal-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.sun-saas-modal-head h2{margin:0;color:#17384d}.sun-saas-modal-head button{border:0;background:#eef1f2;border-radius:50%;width:34px;height:34px;font-size:20px}.sun-saas-plans{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:16px}.sun-saas-plan{border:1px solid #dfe5e8;border-radius:14px;padding:15px;background:#fafcfc}.sun-saas-plan.current{border:2px solid #ffb400;background:#fffaf0}.sun-saas-plan h3{margin:0 0 5px;color:#18384b}.sun-saas-plan ul{padding-left:18px;color:#4f5b64;font-size:12px;line-height:1.55}.sun-saas-plan .muted{color:#89939a}.sun-saas-upgrade{margin-top:12px;background:#eef4f6;border-radius:12px;padding:12px;color:#52606a} + .sun-saas-modal{position:fixed;inset:0;z-index:12100;background:#0d2130c9;display:grid;place-items:center;padding:18px}.sun-saas-modal-card{width:min(900px,100%);max-height:92vh;overflow:auto;background:#fff;border-radius:18px;padding:20px;box-shadow:0 30px 90px #0007}.sun-saas-modal-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.sun-saas-modal-head h2{margin:0;color:#17384d}.sun-saas-modal-head button{border:0;background:#eef1f2;border-radius:50%;width:34px;height:34px;font-size:20px}.sun-saas-plans{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:16px}.sun-saas-plan{border:1px solid #dfe5e8;border-radius:14px;padding:15px;background:#fafcfc}.sun-saas-plan.current{border:2px solid #ffb400;background:#fffaf0}.sun-saas-plan h3{margin:0 0 5px;color:#18384b}.sun-saas-plan ul{padding-left:18px;color:#4f5b64;font-size:12px;line-height:1.55}.sun-saas-plan .muted{color:#89939a}.sun-saas-upgrade{margin-top:12px;background:#eef4f6;border-radius:12px;padding:12px;color:#52606a} .sun-saas-settings-card .sun-saas-status-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px;margin-top:12px}.sun-saas-kpi{border:1px solid #e2e7ea;border-radius:11px;padding:10px;background:#fafcfc}.sun-saas-kpi small{display:block;color:#7a858d;margin-bottom:4px}.sun-saas-kpi b{color:#203846} #sun-saas-admin{padding:20px}.sun-saas-admin-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}.sun-saas-admin-table{display:grid;gap:10px;margin-top:16px}.sun-saas-company{border:1px solid #dfe5e8;border-radius:14px;padding:14px;background:#fff}.sun-saas-company-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;flex-wrap:wrap}.sun-saas-company h3{margin:0;color:#18384b}.sun-saas-company small{color:#78828a}.sun-saas-company-actions{display:grid;grid-template-columns:1fr 110px auto auto;gap:8px;margin-top:11px;align-items:end}.sun-saas-company-actions label{font-size:11px;color:#64717a}.sun-saas-company-actions select{width:100%}.sun-saas-company-meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}.sun-saas-pill{border-radius:999px;padding:5px 8px;background:#eef3f5;font-size:11px}.sun-saas-pill.good{background:#eaf7ef;color:#347153}.sun-saas-pill.warn{background:#fff5dc;color:#8a681b}.sun-saas-pill.bad{background:#ffebe9;color:#993f3d} .sun-saas-onboarding-tabs{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.sun-saas-onboarding-choice{border:1px solid #dfe5e8;background:#fafcfc;border-radius:12px;padding:12px;text-align:left}.sun-saas-onboarding-choice b{display:block;color:#17384d;margin-bottom:3px}.sun-saas-onboarding-choice small{color:#6f7980;line-height:1.35} @@ -4664,7 +4676,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; } function removeSubscriptionUi(){ - $('sunSaaSBadgeV16')?.remove();$('sunSaaSReadonlyV16')?.remove();$('sunSaaSBlockedV16')?.remove();$('sunSaaSSettingsCardV16')?.remove(); + $('sunSaaSBadgeV16')?.remove();$('sunSaaSReadonlyV16')?.remove();$('sunSaaSBlockedV16')?.remove();$('sunSaaSSettingsCardV16')?.remove();$('sunSaaSPlansModalV16')?.remove(); qa('[data-sun-plan-locked]').forEach(el=>{el.removeAttribute('data-sun-plan-locked');el.classList.remove('sun-plan-locked')}); } @@ -4711,17 +4723,23 @@ window.SUN_LEGACY_CATALOG_V175=[]; if(!brand)return; if(!badge){badge=document.createElement('div');badge.id='sunSaaSBadgeV16';badge.className='sun-saas-badge';brand.insertAdjacentElement('afterend',badge);} const d=daysLeft(), trial=snapshot.status==='trialing'; - badge.innerHTML=`${esc(snapshot.plan_name||PLAN_LABELS[snapshot.plan_id]||'Тариф')}${trial?`Пробный период${d!=null?` · ${Math.max(0,d)} дн.`:''}`:accessLabel(snapshot.access_mode)}`; + const html=`${esc(snapshot.plan_name||PLAN_LABELS[snapshot.plan_id]||'Тариф')}${trial?`Пробный период${d!=null?` · ${Math.max(0,d)} дн.`:''}`:accessLabel(snapshot.access_mode)}`; + if(badge.innerHTML!==html)badge.innerHTML=html; badge.onclick=()=>showPlans();badge.title='Тариф и подписка';badge.style.cursor='pointer'; } function applyAccessMode(){ - $('sunSaaSReadonlyV16')?.remove();$('sunSaaSBlockedV16')?.remove(); + if(snapshot?.access_mode!=='read_only')$('sunSaaSReadonlyV16')?.remove(); + if(snapshot?.access_mode!=='blocked')$('sunSaaSBlockedV16')?.remove(); if(!snapshot)return; applyReadOnlyControls(); if(snapshot.access_mode==='read_only'){ - const b=document.createElement('div');b.id='sunSaaSReadonlyV16';b.className='sun-saas-readonly-banner';b.textContent=`Подписка закончилась. До ${formatDate(snapshot.grace_until)} база доступна только для просмотра.`;document.body.appendChild(b); + let b=$('sunSaaSReadonlyV16'); + if(!b){b=document.createElement('div');b.id='sunSaaSReadonlyV16';b.className='sun-saas-readonly-banner';document.body.appendChild(b);} + const text=`Подписка закончилась. До ${formatDate(snapshot.grace_until)} база доступна только для просмотра.`; + if(b.textContent!==text)b.textContent=text; }else if(snapshot.access_mode==='blocked'){ + if($('sunSaaSBlockedV16'))return; const x=document.createElement('div');x.id='sunSaaSBlockedV16';x.className='sun-saas-blocked';x.innerHTML=`
Подписка закончилась. Все данные сохранены и будут доступны сразу после продления.
Подписка относится ко всей компании. Данные этой рабочей базы изолированы от других компаний.
Подписка относится ко всей компании. Данные этой рабочей базы изолированы от других компаний.
Название, порядок, цвет и видимость разделов каталога. Скрытые разделы и их товары не удаляются.

Этот аккаунт управляет платформой и не обязан иметь собственную рабочую компанию.
`; - card.querySelector('[data-open-dev-v1763]')?.addEventListener('click',()=>{patchDeveloperOpen();dev.open?.();}); - card.querySelector('[data-signout-v1763]')?.addEventListener('click',()=>cloud.signOut?.()); - return true; - }finally{checkingGate=false;} - } + // Authentication screens belong to the unified auth gate. Never replace + // its loading/error state with an old developer-only login card. function interceptSaasAdmin(event){ const button=event.target?.closest?.('[data-saas-admin]'); @@ -58,7 +28,6 @@ function maintain(){ patchDeveloperOpen(); - enhanceDeveloperGate().catch(()=>{}); } document.addEventListener('click',interceptSaasAdmin,true); @@ -67,5 +36,5 @@ setTimeout(maintain,0); setInterval(()=>{if(!document.hidden)maintain();},10000); - window.SunHotfixV1763={VERSION,patchDeveloperOpen,enhanceDeveloperGate}; + window.SunHotfixV1763={VERSION,patchDeveloperOpen}; })(); diff --git a/public/core/ops-ux-v1762.js b/public/core/ops-ux-v1762.js index 18896e4..54cbcc7 100644 --- a/public/core/ops-ux-v1762.js +++ b/public/core/ops-ux-v1762.js @@ -130,7 +130,9 @@ pane.innerHTML=``; } function dockEditor(){ + if(!menuActive||!menuView?.classList.contains('on'))return; const pane=$('sunMenuDetailV1762'),home=$('editor'),dialog=home?.querySelector('.dialog')||editorDialog;if(!pane||!home||!dialog)return; + if(dialog.parentElement===pane)return; editorHome=home;editorDialog=dialog;home.classList.remove('on');pane.innerHTML='';pane.classList.add('sun-menu-editor-pane');pane.appendChild(dialog);dialog.classList.add('sun-menu-editor-docked'); } function renderMenuList(){ diff --git a/public/core/performance.js b/public/core/performance.js index 49904cb..33671d1 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; const VERSION='17.7.3'; - const RELEASE='20260918-mobile-calendar'; + const RELEASE='20260918-ui-stability'; const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}}; function installAuthBoot(){ diff --git a/public/index.html b/public/index.html index fe8b257..5146810 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ - - + diff --git a/public/service-worker.js b/public/service-worker.js index 9a71fbd..f2f8c45 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,5 +1,5 @@ -const CACHE='sun-catering-pwa-v109-20260918-mobile-calendar'; -const VERSION='20260918-mobile-calendar'; +const CACHE='sun-catering-pwa-v110-20260918-ui-stability'; +const VERSION='20260918-ui-stability'; const CORE=[ './vendor/supabase-2.112.4.min.js', `./core/help-center.js?v=${VERSION}`,`./core/help-center.css?v=${VERSION}`,`./help/knowledge-v1.json?v=${VERSION}`, diff --git a/tests/app.spec.mjs b/tests/app.spec.mjs index 0a00fe9..a52f19a 100644 --- a/tests/app.spec.mjs +++ b/tests/app.spec.mjs @@ -80,7 +80,7 @@ test('v17.6.2 operations UX boots with menu and route features', async ({ page } expect(checks.calendarMore).toBeTruthy(); expect(await page.locator('header nav button', {hasText:'Меню'}).count()).toBeGreaterThan(0); }); -test('v17.6.3 developer gate bypasses workspace loading and SaaS click is safe', async ({ page }) => { +test('developer tools preserve the unified loading gate and SaaS click is safe', async ({ page }) => { // This is a module fixture: real auth timers must not overwrite its mocked session. await page.setContent(''); await page.evaluate(()=>{ @@ -100,12 +100,12 @@ test('v17.6.3 developer gate bypasses workspace loading and SaaS click is safe', }); const content=fs.readFileSync(path.join(process.cwd(),'public','core','hotfix-v1763.js'),'utf8'); await page.addScriptTag({content}); - await page.waitForFunction(()=>document.querySelector('[data-open-dev-v1763]')&&Boolean(window.SunHotfixV1763),null,{timeout:5000}); - expect(await page.locator('#sunCloudAuthGateV3 h2').textContent()).toBe('Аккаунт разработчика'); + await page.waitForFunction(()=>Boolean(window.SunHotfixV1763),null,{timeout:5000}); + expect(await page.locator('#sunCloudAuthGateV3 h2').textContent()).toBe('Загружаю рабочую базу'); await page.locator('[data-saas-admin]').click(); - await page.locator('[data-open-dev-v1763]').click(); + await expect(page.locator('[data-open-dev-v1763]')).toHaveCount(0); const args=await page.evaluate(()=>window.__devOpenArgs); - expect(args).toEqual(['null','null']); + expect(args).toEqual(['null']); }); test('v17.6.4 auto-completes and fully pays an order one minute after scheduled time', async ({ page }) => { await page.addInitScript(()=>{ diff --git a/tests/playwright.config.mjs b/tests/playwright.config.mjs index b8d1760..f2865c5 100644 --- a/tests/playwright.config.mjs +++ b/tests/playwright.config.mjs @@ -2,13 +2,13 @@ import { defineConfig, devices } from '@playwright/test'; import {fileURLToPath} from 'node:url'; export default defineConfig({ testDir:'.', - testMatch:['help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'], + testMatch:['ui-stability.spec.mjs','help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'], timeout:30000, use:{baseURL:'http://127.0.0.1:4173'}, webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true}, projects:[ {name:'iphone-pdf',testMatch:['proposal-quality.spec.mjs'],grep:/all six selections|transparent wide|an actual offer downloads/,use:{...devices['iPhone 13'],serviceWorkers:'block'}}, - {name:'iphone-webkit',testMatch:['help-center.spec.mjs','login-recovery.spec.mjs','account-access.spec.mjs','calendar-print.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}}, + {name:'iphone-webkit',testMatch:['ui-stability.spec.mjs','help-center.spec.mjs','login-recovery.spec.mjs','account-access.spec.mjs','calendar-print.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}}, {name:'desktop',use:{...devices['Desktop Chrome']}}, {name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}} ] diff --git a/tests/release-check.mjs b/tests/release-check.mjs index 34bbc31..acc89dd 100644 --- a/tests/release-check.mjs +++ b/tests/release-check.mjs @@ -14,15 +14,15 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent') check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer'); check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox'); check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4'); -check(sw.includes('v109-20260918-mobile-calendar')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3'); -check(index.includes('20260918-mobile-calendar')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); +check(sw.includes('v110-20260918-ui-stability')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3'); +check(index.includes('20260918-ui-stability')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3'); check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned'); check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned'); check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded'); check(!performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'),'Developer Console refresh is not mutation-driven'); check(performance.includes('ux-fixes-v1764.js')&&performance.includes('SunUXFixV1764'),'v17.6.4 UX module is loaded'); check(performance.includes('hotfix-v1763.js')&&performance.includes('SunHotfixV1763'),'developer/SaaS hotfix is loaded'); -check(hotfix.includes('patchDeveloperOpen')&&hotfix.includes('enhanceDeveloperGate')&&hotfix.includes('data-saas-admin'),'developer gate and SaaS click hotfix is versioned'); +check(hotfix.includes('patchDeveloperOpen')&&!hotfix.includes('card.innerHTML')&&hotfix.includes('data-saas-admin'),'SaaS click hotfix preserves the unified auth gate'); check(hotfix.includes('source instanceof HTMLElement')&&hotfix.includes('stopImmediatePropagation'),'SaaS event object cannot reach Developer Console as a nav button'); check(performance.includes('ops-ux-v1762.js'),'operations UX module is loaded by performance core'); check(ops.includes('supportReadPermission')&&ops.includes('SUPPORT_POLL_MS=20000')&&ops.includes('refreshSupportWorkspace'),'developer support read-only refresh is lightweight and bounded'); @@ -39,12 +39,12 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30 check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match'); check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json'); check(releaseManifest.channel==='production','release manifest channel is production'); -check(String(releaseManifest.pwaCache||'').includes('v109-20260918-mobile-calendar'),'release manifest points to current PWA cache'); +check(String(releaseManifest.pwaCache||'').includes('v110-20260918-ui-stability'),'release manifest points to current PWA cache'); check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3'); check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards'); check(runtime.includes('refreshSupportWorkspace')&&runtime.includes('sun_dev_support_snapshot'),'cloud exposes lightweight read-only support refresh'); -check(runtime.includes('DEV_ADMIN_TTL_MS=30000')&&hotfix.includes('checkPlatformAdmin?.(false)')&&hotfix.includes('},10000);'),'developer access checks are throttled'); +check(runtime.includes('DEV_ADMIN_TTL_MS=30000')&&!hotfix.includes('checkPlatformAdmin'),'developer access checks are throttled'); check(performance.includes('pendingImageRoots')&&performance.includes('queueImageScan')&&ux.includes('},5000);'),'background DOM maintenance is batched/throttled'); check(runtime.includes('explicitTemplate')&&runtime.includes("OFFER_TEMPLATE_IDS.has(explicitTemplate)"),'per-order proposal template survives render and PDF'); check(runtime.includes("else if(id==='midnight-glass')")&&runtime.includes("else if(id==='emerald-gold')")&&runtime.includes("if(id==='editorial-grid')"),'existing proposal layout sequences remain available'); @@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer'); check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes'); check(pkg.version==='17.7.3','package version is v17.7.3'); -check(index.includes('20260918-mobile-calendar'),'index cache bust is v17.7.3'); -check(sw.includes('v109-20260918-mobile-calendar')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); +check(index.includes('20260918-ui-stability'),'index cache bust is v17.7.3'); +check(sw.includes('v110-20260918-ui-stability')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules'); check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist'); check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active'); check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3'); diff --git a/tests/static-security.mjs b/tests/static-security.mjs index 3258545..9001f35 100644 --- a/tests/static-security.mjs +++ b/tests/static-security.mjs @@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos'); const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x)); if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed'); -if(!sw.includes('20260918-mobile-calendar')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh'); -if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-mobile-calendar')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); +if(!sw.includes('20260918-ui-stability')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh'); +if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-ui-stability')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current'); if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present'); if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present'); if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed'); @@ -41,11 +41,11 @@ if(!serverAutomation.includes("const VERSION='17.7.0'")||!serverAutomation.inclu if(!performance.includes('ux-fixes-v1764.js')||!performance.includes('SunUXFixV1764'))fail('UX fix loader missing');else ok('UX fix loader present'); if(!performance.includes('hotfix-v1763.js')||!performance.includes('SunHotfixV1763'))fail('v17.6.3 hotfix loader missing');else ok('v17.6.3 hotfix loader present'); if(!performance.includes('ops-ux-v1762.js')||!performance.includes('SunOpsUXV1762'))fail('ops UX loader missing');else ok('ops UX loader present'); -for(const marker of ['patchDeveloperOpen','enhanceDeveloperGate','data-saas-admin','stopImmediatePropagation','instanceof HTMLElement']) if(!hotfix.includes(marker))fail(`developer/SaaS hotfix marker missing: ${marker}`);else ok(`developer/SaaS hotfix marker: ${marker}`); +for(const marker of ['patchDeveloperOpen','data-saas-admin','stopImmediatePropagation','instanceof HTMLElement']) if(!hotfix.includes(marker))fail(`developer/SaaS hotfix marker missing: ${marker}`);else ok(`developer/SaaS hotfix marker: ${marker}`); for(const marker of ['SUPPORT_POLL_MS=20000','supportReadPermission','refreshSupportWorkspace','sun-menu-editor-v1762','showCalendarDay',"ROUTE_BASE_KEY='sunRouteBaseV1'",'showRouteOrder','routeOpenYandex']) if(!ops.includes(marker)) fail(`ops UX marker missing: ${marker}`); else ok(`ops UX marker: ${marker}`); for(const marker of ["AUTO_DELAY_MS=60*1000","order.prepayment=total","order.status='Отдан заказчику'",'sunAutoCompletedAt','classificationDate','persistOfferTemplate','clientOfferTemplateId','offerTemplateId','sun-v1764-menu-icon','CateriumServerAutomationV1770?.enabled']) if(!ux.includes(marker))fail(`UX compatibility marker missing: ${marker}`);else ok(`UX compatibility marker: ${marker}`); for(const marker of ['CLOUD_RPC_TIMEOUT_MS=45000','CLOUD_CONFLICT_MAX_RETRIES=4','refreshSupportWorkspace',"const VERSION = '17.7.3'",'ERROR_DEDUPE_MS=5*60*1000','DEV_ADMIN_TTL_MS=30000']) if(!runtime.includes(marker))fail(`stability marker missing: ${marker}`);else ok(`stability marker: ${marker}`); -if(!hotfix.includes('checkPlatformAdmin?.(false)')||!hotfix.includes('},10000);'))fail('Developer fallback polling is still aggressive');else ok('Developer fallback polling is throttled'); +if(hotfix.includes('card.innerHTML')||hotfix.includes('checkPlatformAdmin'))fail('Legacy developer login override returned');else ok('Developer hotfix does not replace the unified login'); if(!ux.includes('sunMenuIconV1766')||!ux.includes('sun-offer-template-mini-editorial-grid')||!runtime.includes('explicitTemplate'))fail('v17.6.6 proposal/menu markers missing');else ok('v17.6.6 proposal/menu markers present'); if(!classic.includes("const VERSION='17.6.7'")||!classic.includes('CLASSIC_IDS')||!classic.includes('ARCHIVE_IDS')||!classic.includes('renderPages'))fail('v17.6.7 classic PDF module missing');else ok('v17.6.7 classic PDF module present'); if(!developerUX.includes("const VERSION='17.6.8'")||!developerUX.includes('sun_dev_delete_company_v1768')||!developerUX.includes('sun_dev_error_groups_v1768')||!developerUX.includes('selectedAccountsWorkspace')||!developerUX.includes('sun-dev-plan-matrix')||!developerUX.includes('KNOWN_DOM_RACE'))fail('v17.6.8 Developer Console UX module missing');else ok('v17.6.8 Developer Console UX module present'); diff --git a/tests/ui-stability.spec.mjs b/tests/ui-stability.spec.mjs new file mode 100644 index 0000000..6a7feb0 --- /dev/null +++ b/tests/ui-stability.spec.mjs @@ -0,0 +1,164 @@ +import fs from 'node:fs'; +import {test,expect} from '@playwright/test'; + +const runtime=fs.readFileSync('public/app-runtime.js','utf8'); +function moduleSource(name){const start=runtime.indexOf(`/* ===== MODULE: ${name} ===== */`),end=runtime.indexOf('/* ===== MODULE:',start+1);return runtime.slice(start,end<0?undefined:end);} +async function fixture(page,body){ + await page.route('**/index.html',r=>r.fulfill({contentType:'text/html; charset=utf-8',body:`${body}`})); + await page.goto('/index.html'); + await page.evaluate(()=>window.SunSafe={escapeHTML:v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])),insertBefore:(p,n,a)=>p.insertBefore(n,a)}); +} + +test('modal stacking respects existing layers and closing never steals input focus',async({page})=>{ + await fixture(page,'