diff --git a/tests/browser/workspace-library.spec.mjs b/tests/browser/workspace-library.spec.mjs index 9c18949c..b91a73df 100644 --- a/tests/browser/workspace-library.spec.mjs +++ b/tests/browser/workspace-library.spec.mjs @@ -57,6 +57,26 @@ test('library groups reusable negatives under one positive without exposing nega for(const item of rows.items) await expect(page.locator(`article[data-prompt-id="${item.id}"] .card-meta > span`).nth(1)).toHaveText(`${item.run_count} run${item.run_count===1?'':'s'}`); }); +// #464: a starved renderer (headless WebKit under CPU load, a background tab) can hold animation frames for seconds; +// a loaded page of prompts is shown without waiting for one. +test('a held animation frame does not hide a loaded page of prompts, the first or a later one',async({page,request})=>{ + await expect(await request.post('/__test__/reset',{data:{native_library:true,extra_prompts:45}})).toBeOK(); + const {total}=await(await request.get('/prompt_manager/library?limit=100')).json(); + await page.addInitScript(()=>{ + const request=window.requestAnimationFrame.bind(window),cancel=window.cancelAnimationFrame.bind(window); + const held=new Map(),real=new Map();let holding=true,next=0; + window.requestAnimationFrame=callback=>{const id=--next;if(holding)held.set(id,callback);else real.set(id,request(callback));return id;}; + window.cancelAnimationFrame=id=>{held.delete(id);if(real.has(id))cancel(real.get(id));real.delete(id);}; + window.__releaseAnimationFrames=()=>{holding=false;for(const [id,callback] of held)real.set(id,request(callback));held.clear();}; + }); + await page.goto(route); + const more=page.getByRole('button',{name:'Load more prompts',exact:true}); + await expect(more).toHaveAttribute('aria-disabled','false');await expect(cards(page)).toHaveCount(40); + await more.evaluate(node=>node.click());await expect(page.locator('.library-status')).toHaveText('You’re all caught up.'); + await expect(cards(page)).toHaveCount(total); + await page.evaluate(()=>window.__releaseAnimationFrames());await nextFrame(page); + await expect(cards(page)).toHaveCount(total);await expect(page.locator('.library-row-spacer')).toHaveCount(0); +}); test('history displays the selected full negative beneath its positive and filters one shared physical gallery',async({page,request})=>{ const rows=await(await request.get('/prompt_manager/library')).json();const cat=rows.items.find(item=>item.text==='Cat portrait'); const data=await(await request.get(`/prompt_manager/library/${cat.id}`)).json(); diff --git a/web/js/workspace/library.mjs b/web/js/workspace/library.mjs index 97de6b37..fe187f9e 100644 --- a/web/js/workspace/library.mjs +++ b/web/js/workspace/library.mjs @@ -394,7 +394,10 @@ export function mountLibrary(main,api,parentScope,{positiveId,savedState=null,on const hidden=hiddenSave!==null && !next.loading && !next.error && !next.hasMore && !next.items.some(item=>item.id===hiddenSave)?' The saved prompt is hidden by the current filters.':''; message.textContent=(next.error?'Prompt library could not be loaded. Retry.':next.loading?'Loading prompt library…':!next.items.length?'No matching prompts. Try changing the filters.':!next.hasMore?'You’re all caught up.':'')+hidden; message.setAttribute('role',next.error?'alert':'status');retry.hidden=!next.error;more.hidden=!next.hasMore || !!next.error;more.setAttribute('aria-disabled',String(next.loading)); - schedule();scheduleLoad(); + // #464: a loaded page is laid out now, not on the next frame: a starved renderer (a background tab, headless + // WebKit under load) can hold frames for seconds, which left a loaded shelf empty. Measurements still flush per frame. + if(batch && grid.isConnected) {cancelAnimationFrame(frame);flush();} else schedule(); + scheduleLoad(); } const stream=createLibraryStream({api,onChange:render,restore:restorable?{filters,state:restorable.state}:undefined}); function maybeLoad() {