fix(images): read a loading collection as loading, never as 0 of 0 images (#391)

A reset published {loading, loaded:0, total:0} and the caption painted it
as "0 of 0 images" until the first page answered, so a fast reader (the
inspector concept spec on WebKit) read an empty result the server never
returned. The count line now says "Loading images…" while a first page is
in flight; the spec waits for the answered count, and a held-listing spec
pins the ordering.
This commit is contained in:
Vito Sansevero
2026-10-27 18:27:30 -07:00
parent 44177974cb
commit 7fc36283f8
3 changed files with 45 additions and 3 deletions
+21 -1
View File
@@ -228,7 +228,8 @@ async function productionSendOf(page,context) {
async function tagOf(page) {
const before=await page.locator('#result-count').textContent();
await panelOf(page).locator('.detail-inner > .tags').last().getByRole('button',{name:'cinematic',exact:true}).click();
await expect.poll(()=>page.locator('#result-count').textContent()).not.toBe(before);
// Production's caption reads "Loading images…" until the tag's listing answers (#391); the concept's search is instant.
await expect.poll(async()=>{const text=await page.locator('#result-count').textContent();return text!==before&&text!=='Loading images…';}).toBe(true);
await settle(page);
return {count:await page.locator('#result-count').textContent(),
cards:await page.locator(`${CARDS} .image-open`).evaluateAll(nodes=>nodes.map(node=>node.getAttribute('aria-label')))};
@@ -328,6 +329,25 @@ test('a recipe-linked image without recipe metadata shows its positive prompt me
await expect(panelOf(page).locator('.detail-footer button')).toHaveText(['Send to ComfyUI','Prompt history']);
});
// #391: a tag chip replaces the collection; until its listing answers the caption says it is loading, never that the
// filter matched nothing ("0 of 0 images" was painted before the response and read by a fast reader).
test('a tag chip collection reads as loading, not empty, until its listing answers (#391)',async({page})=>{
await page.setViewportSize({width:1600,height:1100});
await openProductionPanel(page,'dark');
let release,arrived;
const held=new Promise(resolve=>{release=resolve;}),reached=new Promise(resolve=>{arrived=resolve;});
await page.route(url=>url.pathname==='/prompt_manager/images/output'&&url.searchParams.get('tags')==='cinematic',
async route=>{arrived();await held;await route.continue();},{times:1});
const count=page.locator('#result-count');
await panelOf(page).locator('.detail-inner > .tags').last().getByRole('button',{name:'cinematic',exact:true}).click();
await reached;
await expect(count).toHaveText('Loading images…');
await expect(page.locator(CARDS)).toHaveCount(0);
release();
await expect(count).toHaveText(countCopy('8 of 8 images · 2 positive prompts'));
await expect(page.locator(CARDS)).toHaveCount(8);
});
// Task C3 (D88, #249 PR C): the concept's chip row is the image's own tags (`i.tags`, app.js a402). With those names
// saved as the file's current image tags, production draws the row from them (Tags filter buttons in the panel, plain
// in the viewer, the linked positive's equal tags not repeated) and still equals the concept, in both sizes.
+17
View File
@@ -234,6 +234,23 @@ test('the count line reads N of M images and K positive prompts only when known'
assert.equal(countLine({loaded:1200,total:3400,positiveCount:0}),'1,200 of 3,400 images · 0 positive prompts');
});
test('the count line reads as loading, never as an empty result, until a first page answers (#391)', async () => {
const {countLine,createImageStream}=await source;
assert.equal(countLine({loaded:0,total:0,positiveCount:null,loading:true,error:null}),'Loading images…');
// A failed first page and a settled empty one say what the server said; a later page keeps the loaded count.
assert.equal(countLine({loaded:0,total:0,positiveCount:null,loading:false,error:new Error('x')}),'0 of 0 images');
assert.equal(countLine({loaded:0,total:0,positiveCount:0,loading:false,error:null}),'0 of 0 images · 0 positive prompts');
assert.equal(countLine({loaded:40,total:102,positiveCount:2,loading:true,error:null}),'40 of 102 images · 2 positive prompts');
// While a reset's first page is in flight the line reads as loading (the cleared state before it is replaced in
// the same task, so it is never painted).
let answer;const lines=[];
const stream=createImageStream({api:{get:()=>new Promise(resolve=>{answer=resolve;})},onChange:state=>lines.push(countLine(state))});
const loading=stream.reset({tags:['cinematic']});
assert.equal(lines.at(-1),'Loading images…');
answer({images:[{path:'a.png',root_index:0}],total:1,has_more:false,next_cursor:null,positive_count:1});await loading;
assert.equal(lines.at(-1),'1 of 1 image · 1 positive prompt');
});
test('the caption date names the newest result day under Newest first only', async () => {
const {captionDate}=await source;
const now=new Date(2026,9,17,15,0);