diff --git a/tests/browser/workflows.spec.mjs b/tests/browser/workflows.spec.mjs index 453a3ac..ab5d842 100644 --- a/tests/browser/workflows.spec.mjs +++ b/tests/browser/workflows.spec.mjs @@ -64,11 +64,16 @@ test('actual Tags page reloads the selected filter and opens its image viewer', await page.goto('/prompt_manager/admin#/tags'); await page.locator('[data-tag-name="cats"]').click(); await expect(page.locator('#tagPromptsGrid')).toContainText('Cat portrait'); + const dogs = page.waitForResponse(response => response.url().includes('/tags/dogs/prompts?')); await page.locator('[data-tag-name="dogs"]').click(); + const [shown] = (await (await dogs).json()).prompts[0].images; await expect(page.locator('#tagPromptsGrid')).toContainText('Dog landscape'); await expect(page.locator('#tagPromptsGrid')).not.toContainText('Cat portrait'); await page.locator('#tagPromptsGrid img').first().click(); await expect(page.locator('.viewer-canvas img')).toBeVisible(); + // The viewer's title names the shown image through the page's title callback. ViewerJS renders it only once the + // image is viewed, so closing at "visible" alone left the callback unrun on slower hosts (CI coverage missed it). + await expect(page.locator('.viewer-title')).toHaveText(shown.filename); await page.locator('.viewer-container').getByRole('button', { name: 'Close', exact: true }).click(); await expect(page.locator('.viewer-container')).toHaveCount(0); }); diff --git a/tests/browser/workspace-library.spec.mjs b/tests/browser/workspace-library.spec.mjs index 0b35746..1b830dd 100644 --- a/tests/browser/workspace-library.spec.mjs +++ b/tests/browser/workspace-library.spec.mjs @@ -25,6 +25,8 @@ async function loadShelfPages(page,more,pages) { await more.evaluate(node=>node.click());await response;await expect(more).toHaveAttribute('aria-disabled','false'); } } +/** Resolves in the rendering frame after every frame callback the page had already queued (callbacks run in request order). */ +const nextFrame=page=>page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>resolve()))); async function openHistory(page,text='Cat portrait') { await cards(page).filter({has:page.getByText(text,{exact:true})}).getByRole('button',{name:'Explore history',exact:true}).click(); await expect(history(page)).toBeVisible(); @@ -103,7 +105,8 @@ test('library infinite loading keeps unique parents and keyboard-accessible hist const appended=await (await nextPage).json();expect(appended.items.length).toBeGreaterThan(0);hasMore=appended.has_more; if(hasMore)await expect(more).toHaveAttribute('aria-disabled','false'); } - await expect(more).toBeHidden();expect(await cards(page).count()).toBeLessThan(80); + // The last page's render queued the window update for the next frame; count after that frame, not before it. + await expect(more).toBeHidden();await nextFrame(page);expect(await cards(page).count()).toBeLessThan(80); const ids=await cards(page).evaluateAll(nodes=>nodes.map(node=>node.dataset.promptId));expect(new Set(ids).size).toBe(ids.length); await expect(page.getByRole('button',{name:/next page|previous page/i})).toHaveCount(0); const firstId=await cards(page).first().getAttribute('data-prompt-id'); @@ -376,14 +379,16 @@ async function observeLibrary(page, hold=false) { holdFrames(){holdingFrames=true;}, releaseFrames(){holdingFrames=false;const callbacks=[...heldFrames.values()];heldFrames.clear();for(const callback of callbacks)callback();}, equal(){for(const record of records){const entries=[...record.latest.values()].filter(entry=>record.targets.has(entry.target));if(entries.length)record.callback(entries,record.native);}}, + // Resolves once two consecutive rendering frames find no Library frame queued. Each check runs inside a native + // frame callback, so a frame the page queues later in that update (a scroll event, a ResizeObserver delivery) is + // still pending there. A timer is not a frame: under CPU load a 60 ms wait could end before the scroll's frame. + settled(){return new Promise(resolve=>{let quiet=0;const check=()=>{quiet=pending.size?0:quiet+1;if(quiet>=2)resolve();else raf(check);};raf(check);});}, stats(){return {requested,pending:pending.size,heldFrames:heldFrames.size,measurements:records.reduce((sum,record)=>sum+record.entries.size,0),mutations,unmounted,targets:records.reduce((sum,record)=>sum+record.targets.size,0)};}, }; },{hold}); } async function settleLibrary(page) { - await expect.poll(async()=>page.evaluate(()=>window.libraryObservation.stats().pending)).toBe(0); - await page.evaluate(()=>new Promise(resolve=>setTimeout(resolve,60))); - await expect.poll(async()=>page.evaluate(()=>window.libraryObservation.stats().pending)).toBe(0); + await page.evaluate(()=>window.libraryObservation.settled()); } async function firstVisibleParent(page) { return cards(page).evaluateAll(nodes=>{