2026-10-16 16:25:42 +08:00
/* Mock-only: known image metadata renders real slots; no synthetic skeletons. */
const assert = require ( 'assert' ), fs = require ( 'fs' ),{ 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 ));
let releaseAccount ; await page . route ( '**/api/v1/auth/me' , async r =>{ await new Promise ( resolve => releaseAccount = resolve ); await r . continue ();});
await page . goto ( base ); await page . locator ( '#nav button' ). first (). waitFor (); assert . equal ( await page . locator ( '.image-pending,.placeholder-line,.page-placeholder' ). count (), 0 ); assert . equal ( await page . locator ( '.page-loading' ). count (), 0 ); releaseAccount ();
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 = 1 ;
for ( const width of [ 1440 , 390 ]){
await page . setViewportSize ({ width , height : 844 }); await page . evaluate (()=> state . galleryCache ? . clear ()); let releaseMetadata , metadataRequested , releaseImage , imageRequested ;
await page . route ( '**/api/v1/devices/*/images' , async r =>{ metadataRequested (); await new Promise ( resolve => releaseMetadata = resolve ); await r . fulfill ({ json : [{ image_id : 1 , name : 'Progressive fixture' , url : '/progressive-fixture.svg' , current : false }]});});
await page . route ( '**/progressive-fixture.svg*' , async r =>{ assert . equal ( await page . locator ( '#gallery-search' ). count (), 1 , 'Search must precede image requests' ); assert ( await page . locator ( '#gallery-search' ). evaluate ( e => typeof e . oninput === 'function' )); imageRequested (); await new Promise ( resolve => releaseImage = resolve ); await r . fulfill ({ contentType : 'image/svg+xml' , body : '<svg xmlns="http://www.w3.org/2000/svg" width="300" height="400"/>' });});
const metadata = new Promise ( resolve => metadataRequested = resolve ); const image = new Promise ( resolve => imageRequested = resolve );
await page . evaluate (()=>{ window . navigation = navigate ( 'gallery' );}); await metadata ;
assert . equal ( await page . locator ( '.image-pending,.placeholder-line,.page-placeholder' ). count (), 0 ); assert . equal ( await page . locator ( '.image-card' ). count (), 0 ); assert . equal ( await page . locator ( '.page-loading' ). count (), 0 ); checks ++ ;
2026-10-16 19:21:20 +08:00
assert ( await page . locator ( '#cloud-tab' ). isVisible ()); assert ( await page . locator ( '#local-tab' ). isEnabled ()); assert ( await page . locator ( '#gallery-upload' ). isEnabled ()); assert ( await page . locator ( '#gallery-search' ). isVisible ()); assert ( await page . locator ( '#gallery-sort' ). isEnabled ()); assert ( await page . locator ( '#gallery-list-status' ). isVisible ()); assert . equal ( await page . locator ( '.card.empty' ). count (), 0 ); await page . locator ( '#gallery-search' ). fill ( 'Progressive' ); await page . locator ( '#gallery-sort' ). selectOption ( 'name' ); checks ++ ;
2026-10-16 16:25:42 +08:00
releaseMetadata (); await image ; await page . evaluate (()=> window . navigation );
2026-10-16 19:21:20 +08:00
assert . equal ( await page . locator ( '#gallery-search' ). inputValue (), 'Progressive' ); assert . equal ( await page . locator ( '#gallery-sort' ). inputValue (), 'name' ); assert ( await page . locator ( '#gallery-upload' ). isEnabled ()); assert . equal ( await page . locator ( '.image-loading .spinner' ). count (), 1 ); assert . equal ( await page . locator ( '.image-pending' ). count (), 0 ); checks ++ ;
2026-10-16 16:25:42 +08:00
assert ( await page . locator ( '.image-loading' ). evaluate ( box =>{ const a = box . getBoundingClientRect (), b = box . querySelector ( '.spinner' ). getBoundingClientRect (); return 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 ;})); checks ++ ;
releaseImage (); await page . waitForFunction (()=> document . querySelector ( '.thumb' ) ? . naturalWidth === 300 &&! document . querySelector ( '.image-loading' )); checks ++ ;
await page . evaluate (()=> navigate ( 'upload' ));
const cachedMetadata = new Promise ( resolve => metadataRequested = resolve ); const cachedImage = new Promise ( resolve => imageRequested = resolve );
await page . evaluate (()=>{ window . navigation = navigate ( 'gallery' );}); await cachedMetadata ; await cachedImage ;
assert . equal ( await page . locator ( '#gallery-search' ). count (), 1 , 'Search must exist while cached metadata refresh is pending' ); assert ( await page . locator ( '#gallery-search' ). evaluate ( e => typeof e . oninput === 'function' ), 'Search must be interactive' ); assert . equal ( await page . locator ( '.image-card' ). count (), 1 ); assert . equal ( await page . locator ( '.image-loading .spinner' ). count (), 1 ); assert . equal ( await page . locator ( '.image-pending,.placeholder-line,.page-placeholder' ). count (), 0 ); checks ++ ;
releaseMetadata (); await page . evaluate (()=> window . navigation ); releaseImage (); await page . waitForFunction (()=> document . querySelector ( '.thumb' ) ? . naturalWidth === 300 &&! document . querySelector ( '.image-loading' ));
await page . unroute ( '**/api/v1/devices/*/images' ); await page . unroute ( '**/progressive-fixture.svg*' );
let releaseDevice , deviceRequested ; await page . route ( '**/api/v1/devices/living-room' , async r =>{ deviceRequested (); await new Promise ( resolve => releaseDevice = resolve ); await r . continue ();});
const device = new Promise ( resolve => deviceRequested = resolve ); await page . evaluate (()=>{ window . navigation = navigate ( 'dashboard' );}); await device ;
assert . equal ( await page . locator ( '.image-pending,.placeholder-line,.page-placeholder' ). count (), 0 ); assert . equal ( await page . locator ( '.page-loading' ). count (), 0 ); checks ++ ;
releaseDevice (); await page . evaluate (()=> window . navigation ); await page . unroute ( '**/api/v1/devices/living-room' );
}
2026-10-16 19:21:20 +08:00
await page . evaluate (()=>{ state . galleryCache ? . clear (); state . tab = 'cloud' ;});
let releaseSlow , slowRequested ; const slow = new Promise ( resolve => slowRequested = resolve );
await page . route ( '**/api/v1/devices/*/images' , async r =>{ slowRequested (); await new Promise ( resolve => releaseSlow = resolve ); await r . fulfill ({ json : []});});
await page . evaluate (()=>{ window . slowNavigation = navigate ( 'gallery' );}); await slow ; await page . locator ( '#local-tab' ). click (); await page . waitForFunction (()=> state . tab === 'local' && document . querySelector ( '#local-tab' ) ? . classList . contains ( 'active' )); releaseSlow (); await page . evaluate (()=> window . slowNavigation ); assert . equal ( await page . evaluate (()=> state . tab ), 'local' ); assert ( await page . locator ( '#gallery-search' ). isVisible ()); checks ++ ;
await page . unroute ( '**/api/v1/devices/*/images' );
await page . evaluate (()=>{ state . galleryCache ? . clear (); state . tab = 'cloud' ;});
await page . route ( '**/api/v1/devices/*/images' , r => r . fulfill ({ status : 503 , json : { error : { code : 'CLOUD_UNAVAILABLE' , message : 'fixture unavailable' }}}));
await page . evaluate (()=> navigate ( 'gallery' )); assert ( await page . locator ( '#gallery-search' ). isVisible ()); assert ( await page . locator ( '#local-tab' ). isEnabled ()); assert . equal ( await page . locator ( '#gallery-list-status' ). getAttribute ( 'role' ), 'alert' ); checks ++ ;
await page . unroute ( '**/api/v1/devices/*/images' );
2026-10-16 16:25:42 +08:00
let releaseJobs , jobsRequested ; await page . route ( '**/api/v1/jobs' , async r =>{ jobsRequested (); await new Promise ( resolve => releaseJobs = resolve ); await r . fulfill ({ json : []});});
await page . route ( '**/api/v1/assets?*' , r => r . fulfill ({ json : [{ asset_id : 'local-fixture' , name : 'Local fixture' , url : '/local-fixture.svg' , origin : 'manual' , starred : false }]}));
await page . route ( '**/local-fixture.svg' , r => r . fulfill ({ contentType : 'image/svg+xml' , body : '<svg xmlns="http://www.w3.org/2000/svg" width="300" height="400"/>' }));
const jobs = new Promise ( resolve => jobsRequested = resolve ); await page . evaluate (()=>{ state . tab = 'local' ; return navigate ( 'gallery' );}); await jobs ;
assert . equal ( await page . locator ( '.asset-select' ). count (), 1 ); assert ( await page . locator ( '#gallery-search' ). evaluate ( e => typeof e . oninput === 'function' )); await page . locator ( '#gallery-search' ). fill ( 'no-match' ); assert ( await page . locator ( '.image-card' ). evaluate ( e => e . hidden )); releaseJobs (); checks ++ ;
assert . deepEqual ( errors ,[]); console . log ( 'Known image positions without skeletons:' , checks , 'checks PASS; delayed bootstrap, metadata and images; no page errors' );
} finally { await browser . close ();}
})(). catch ( e =>{ console . error ( e . message ); process . exit ( 1 )});