/* Isolated Mock UI audit: delayed images stay inside their own placeholders. */ const assert=require('assert'); const fs=require('fs'); const {chromium}=require('playwright'); (async()=>{ const base=process.env.FRAME_FLOW_URL;assert(base); const token=fs.readFileSync(process.env.FRAME_FLOW_ENV_FILE,'utf8').split('\n').find(x=>x.startsWith('FRAME_FLOW_TOKEN=')).slice(17); assert((await(await fetch(base+'/api/v1/settings',{headers:{Authorization:'Bearer '+token}})).json()).mock===true); const browser=await chromium.launch({headless:true,executablePath:process.env.CHROME_EXECUTABLE||'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'}); try{ const page=await browser.newPage();const errors=[];page.on('pageerror',e=>errors.push(e.message)); await page.route('**/api/v1/source-libraries',r=>r.fulfill({json:[{id:'fixture',name:'Fixture',kind:'synology-photos'}]})); await page.route('**/api/v1/generation-plans?*',r=>r.fulfill({json:[]})); await page.route('**/api/v1/source-libraries/fixture/images?*',r=>r.fulfill({json:{total:1,items:[{id:'fixture-image',name:'Loading fixture',url:'/ui-audit-image.svg',created_at:'2026-10-23',versions:[]}]}})); let release;let requested; await page.route('**/ui-audit-image.svg*',async r=>{requested();await new Promise(resolve=>release=resolve);await r.fulfill({contentType:'image/svg+xml',body:''});}); await page.goto(base);await page.locator('#username').fill(process.env.FRAME_FLOW_TEST_USERNAME);await page.locator('#password').fill(process.env.FRAME_FLOW_TEST_PASSWORD);await page.locator('#login-form button').click();await page.locator('.hero').waitFor(); let checks=0; async function centered(){ const result=await page.locator('.image-loading').evaluate(box=>{ const spinner=box.querySelector('.spinner'),img=box.querySelector('img'),a=box.getBoundingClientRect(),b=spinner.getBoundingClientRect(); return {centered:Math.abs(a.x+a.width/2-b.x-b.width/2)<1&&Math.abs(a.y+a.height/2-b.y-b.height/2)<1,hidden:getComputedStyle(img).visibility==='hidden',contained:box.querySelector('.image-loading-status').offsetParent===box,overflow:document.documentElement.scrollWidth>innerWidth}; });assert.deepStrictEqual(result,{centered:true,hidden:true,contained:true,overflow:false});checks++; assert.equal(await page.locator('.spinner:not(.image-loading .spinner)').count(),0);checks++; } for(const width of [1440,768,390])for(const theme of ['light','dark']){ await page.setViewportSize({width,height:900});await page.evaluate(theme=>document.documentElement.dataset.theme=theme,theme); const request=new Promise(resolve=>requested=resolve);await page.evaluate(()=>navigate('gallery'));await request;await centered(); const before=await page.locator('.image-loading').boundingBox();release();await page.waitForFunction(()=>document.querySelector('.thumb')?.naturalWidth===300&&!document.querySelector('.image-loading')); const after=await page.locator('.thumb').boundingBox();assert(Math.abs(before.height-after.height)<1,'Thumbnail placeholder changes height');checks++; const originalRequest=new Promise(resolve=>requested=resolve);await page.locator('[data-library-preview]').first().click();await originalRequest;await centered();release();await page.waitForFunction(()=>document.querySelector('#original-image')?.naturalWidth===300&&!document.querySelector('.image-loading'));await page.locator('#modal-cancel').click();checks++; await page.evaluate(async()=>{const img=document.querySelector('.thumb');await blobImage(img,'/ui-audit-missing-image');}); assert.equal(await page.locator('.image-load-error').count(),1);assert.equal(await page.locator('.image-load-error .spinner').count(),0);assert(await page.locator('.image-load-error').innerText());checks++; } await page.evaluate(()=>navigate('connections')); assert.deepStrictEqual(await page.locator('#main input,#main select,#main textarea').evaluateAll(nodes=>nodes.filter(e=>e.checkVisibility()&&!e.labels.length&&!e.getAttribute('aria-label')&&!e.getAttribute('aria-labelledby')).map(e=>e.id)),[]);checks++; assert.equal(await page.locator('nav [aria-current="page"]').count(),1);checks++; const nav=await page.locator('nav button').evaluateAll(nodes=>nodes.map(e=>({height:e.getBoundingClientRect().height,nowrap:getComputedStyle(e).whiteSpace})));assert(nav.every(e=>e.height>=44&&e.nowrap==='nowrap'));checks++; assert.deepStrictEqual(errors,[]);console.log('Image/UI consistency audit:',checks,'passed; desktop/tablet/mobile, light/dark; no page errors'); }finally{await browser.close();} })().catch(e=>{console.error(e);process.exit(1)});