Files
ComfyAssets-ComfyUI_PromptM…/tests/browser/workspace-library.spec.mjs
T
Vito fdba6fd012 feat(workspace): group saved negatives in the native Prompt Library (#210)
* feat(workspace): group saved negative variants in native library (#208)

* fix(library): preserve full name order and batch previews (#208)
2026-10-17 14:38:03 -07:00

171 lines
14 KiB
JavaScript

import {startCoverage,saveCoverage} from '../../tools/vendor/browser-coverage.mjs';
import {test,expect} from '../../tools/vendor/node_modules/@playwright/test/index.mjs';
const route='/prompt_manager/workspace#/library';
const cards=page=>page.locator('article[data-prompt-id]');
const history=page=>page.getByRole('region',{name:'Prompt history',exact:true});
const saved=page=>history(page).getByRole('region',{name:'Saved prompt texts',exact:true});
const gallery=page=>page.getByRole('region',{name:'Shared gallery',exact:true});
const media=page=>gallery(page).locator('article[data-image-key]');
async function openHistory(page,text='Cat portrait') {
await cards(page).filter({has:page.getByText(text,{exact:true})}).getByRole('link',{name:'Explore history',exact:true}).click();
await expect(history(page)).toBeVisible();
}
async function selectVariant(page,index) {
const choice=history(page).getByRole('button',{name:`Select negative variant ${index+1}`,exact:true});
await choice.click();await expect(choice).toHaveAttribute('aria-pressed','true');
}
test.beforeEach(async({page,request,browserName})=>{
await startCoverage(page,browserName);await expect(await request.post('/__test__/reset',{data:{native_library:true}})).toBeOK();
page.libraryErrors=[];page.on('pageerror',error=>page.libraryErrors.push(error.message));
});
test.afterEach(async({page,browserName})=>{await saveCoverage(page,browserName);expect(page.libraryErrors).toEqual([]);});
test('library groups reusable negatives under one positive without exposing negative-only components as parents',async({page})=>{
await page.goto(route);await expect(page.locator('#workspace-main h1')).toHaveText('Prompt library');
await expect(cards(page)).toHaveCount(3);
await expect(cards(page).filter({has:page.getByText('Cat portrait',{exact:true})})).toHaveCount(1);
await expect(cards(page).filter({has:page.getByText('Dual role portrait',{exact:true})})).toHaveCount(1);
await expect(cards(page).filter({hasText:'low contrast'})).toHaveCount(0);
const cat=cards(page).filter({has:page.getByText('Cat portrait',{exact:true})});
await expect(cat).toContainText(/4.*variants/i);await expect(cat).toContainText(/3.*images/i);
await expect(page.locator('#workspace-main')).not.toContainText(/\b\d+ (?:tracked )?runs\b/i);
});
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();
await page.goto(route);await openHistory(page);await expect(media(page)).toHaveCount(3);
for(const negative of ['blurry, watermark','low quality','low contrast','']) {
const index=data.variants.findIndex(variant=>variant.negative_text===negative);await selectVariant(page,index);
const neg=saved(page).getByRole('region',{name:'Negative prompt',exact:true});
await expect(neg).toContainText(negative||'Empty negative prompt');
await expect(media(page)).toHaveCount(negative==='blurry, watermark'||negative==='low quality'?1:0);
const positive=saved(page).getByRole('region',{name:'Positive prompt',exact:true});
await expect(positive).toContainText('Cat portrait');
const order=await positive.evaluate((element)=>element.compareDocumentPosition(element.parentElement.querySelector('[aria-label="Negative prompt"]')));
expect(order&4).toBeTruthy();
}
await history(page).getByRole('button',{name:'All images for this positive',exact:true}).click();await expect(media(page)).toHaveCount(3);
expect(await media(page).evaluateAll(nodes=>nodes.map(node=>node.dataset.imageKey))).toEqual(expect.arrayContaining(['0:shared/same.png','1:shared/same.png','0:legacy-unassigned.png']));
});
test('search, category and tag filters preserve their constraints and return standalone positives',async({page})=>{
await page.goto(route);await page.getByRole('searchbox',{name:'Search prompts',exact:true}).fill('Dog landscape');
await expect(cards(page)).toHaveCount(1);await expect(cards(page).first()).toContainText('Dog landscape');
await page.getByRole('searchbox',{name:'Search prompts',exact:true}).fill('');
await page.getByRole('combobox',{name:'Category',exact:true}).selectOption('Portraits');
await page.getByRole('textbox',{name:'Tags',exact:true}).fill('studio');await expect(cards(page)).toHaveCount(1);
await page.getByRole('textbox',{name:'Tags',exact:true}).fill('outdoors');await expect(cards(page)).toHaveCount(0);
await page.getByRole('combobox',{name:'Category',exact:true}).selectOption('');await expect(cards(page)).toHaveCount(1);
});
test('library infinite loading keeps unique parents and keyboard-accessible history controls',async({page,request})=>{
await expect(await request.post('/__test__/reset',{data:{native_library:true,extra_prompts:125}})).toBeOK();
await page.addInitScript(()=>{window.IntersectionObserver=undefined;});
await page.goto(route);await expect(cards(page).first()).toBeVisible();
const more=page.getByRole('button',{name:'Load more prompts',exact:true});
expect(await cards(page).count()).toBeLessThan(128);
let hasMore=true;
while(hasMore) {
const nextPage=page.waitForResponse(response=>{const url=new URL(response.url());return url.pathname==='/prompt_manager/library'&&Boolean(url.searchParams.get('cursor'));});
await more.focus();await page.keyboard.press('Enter');
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);
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');
const explore=page.locator(`article[data-prompt-id="${firstId}"]`).getByRole('link',{name:'Explore history',exact:true});
await explore.focus();await expect(explore).toBeFocused();await page.keyboard.press('Enter');await expect(history(page)).toBeVisible();
});
test('failed library reads retry while late search responses cannot overwrite newer results',async({page})=>{
let fail=true;let release;const hold=new Promise(resolve=>{release=resolve;});
await page.route('**/prompt_manager/library?*',async request=>{
const url=new URL(request.request().url());
if(fail)return request.fulfill({status:503,json:{success:false,error:'Unavailable'}});
if(url.searchParams.get('text')==='Cat')await hold;
await request.continue().catch(()=>{});
});
await page.goto(route);await expect(page.getByRole('button',{name:'Retry prompt library',exact:true})).toBeVisible();
fail=false;await page.getByRole('button',{name:'Retry prompt library',exact:true}).click();await expect(cards(page)).toHaveCount(3);
const search=page.getByRole('searchbox',{name:'Search prompts',exact:true});const oldRequest=page.waitForRequest(request=>new URL(request.url()).searchParams.get('text')==='Cat');await search.fill('Cat');await oldRequest;await search.fill('Dog');
await expect(cards(page)).toHaveCount(1);await expect(cards(page).first()).toContainText('Dog landscape');release();await expect(cards(page).first()).toContainText('Dog landscape');
});
test('returning from history restores library context and mobile content stays inside the viewport',async({page})=>{
await page.setViewportSize({width:390,height:844});await page.goto(route);
await page.getByRole('combobox',{name:'Category',exact:true}).selectOption('Animals');
await page.getByRole('searchbox',{name:'Search prompts',exact:true}).fill('Cat portrait');await expect(cards(page)).toHaveCount(1);
await openHistory(page);await selectVariant(page,0);
await expect(saved(page).getByRole('region',{name:'Positive prompt',exact:true})).toContainText('Cat portrait');
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true);
await history(page).getByRole('link',{name:'Back to library',exact:true}).click();
await expect(page.getByRole('searchbox',{name:'Search prompts',exact:true})).toHaveValue('Cat portrait');await expect(cards(page)).toHaveCount(1);
await page.getByRole('navigation',{name:'Workspace',exact:true}).getByRole('link',{name:'Overview',exact:true}).click();
await expect(history(page)).toHaveCount(0);await expect(page.locator('dialog[open]')).toHaveCount(0);
});
test('history detail failures retry and returning to the list ignores late detail responses',async({page})=>{
let fail=true;let release;const hold=new Promise(resolve=>{release=resolve;});
await page.route(/\/prompt_manager\/library\/\d+(?:\?|$)/,async route=>{
if(fail)return route.fulfill({status:503,json:{success:false,error:'Unavailable'}});
await hold;await route.continue().catch(()=>{});
});
try {
await page.goto(route);await cards(page).filter({has:page.getByText('Cat portrait',{exact:true})}).getByRole('link',{name:'Explore history',exact:true}).click();
await expect(page.getByRole('button',{name:'Retry prompt history',exact:true})).toBeVisible();
fail=false;await page.getByRole('button',{name:'Retry prompt history',exact:true}).click();
await page.getByRole('link',{name:'Back to library',exact:true}).click();release();
await expect(cards(page)).toHaveCount(3);await expect(history(page)).toHaveCount(0);
} finally {release();}
});
test('history shares real image metadata, vendored Viewer and the acknowledged prompt sender',async({page,context,request})=>{
const host=await context.newPage();await host.goto('/prompt_manager/workspace#/overview');
await host.evaluate(async()=>{
const {createBridgeHost}=await import('/prompt_manager/js/shared/toolkit/prompt-bridge.js');
const target={id:'pair-1',label:'Library destination',type:'PromptManagerPair',roles:['positive','negative']};
window.libraryApplied=[];
window.libraryHost=createBridgeHost({origin:location.origin,namespace:'/',label:'Library ComfyUI',adapter:{
listTargets:()=>[target],preview:(_id,roles)=>({preview:{target,fields:Object.entries(roles).map(([role,after])=>({role,before:`Previous ${role}`,after})),composition:{prepend:'',append:'',prependConnected:false,appendConnected:false,before:'Previous positive',after:roles.positive??'Previous positive'}},snapshot:roles}),
apply:roles=>{window.libraryApplied.push(roles);return{targetId:target.id,values:roles};},
}});
});
try {
await page.goto(route);
const parentId=await cards(page).filter({has:page.getByText('Cat portrait',{exact:true})}).getAttribute('data-prompt-id');
const recipe=await(await request.get(`/prompt_manager/library/${parentId}`)).json();
await openHistory(page);await selectVariant(page,recipe.variants.findIndex(variant=>variant.negative_text==='blurry, watermark'));
const chosenNegative=await saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre').textContent();
await saved(page).getByRole('button',{name:'Send prompt pair',exact:true}).click();
const sender=page.getByRole('dialog',{name:'Send to ComfyUI',exact:true});
const hosts=sender.getByRole('combobox',{name:'ComfyUI window',exact:true});
await expect(hosts.locator('option')).toHaveCount(2);await hosts.selectOption({label:'Library ComfyUI'});
await sender.getByRole('combobox',{name:'Destination node',exact:true}).selectOption({label:'Library destination'});
await sender.getByRole('button',{name:'Preview changes',exact:true}).click();await expect(sender).toContainText(chosenNegative||'(empty)');
await sender.getByRole('button',{name:'Apply to ComfyUI',exact:true}).click();await expect(sender).toContainText('Sent to ComfyUI.');
expect(await host.evaluate(()=>window.libraryApplied)).toEqual([{positive:'Cat portrait',negative:chosenNegative}]);
await sender.getByRole('button',{name:'Close',exact:true}).click();
await media(page).first().getByRole('button',{name:'Open same.png',exact:true}).click();
const inspector=page.getByRole('region',{name:'Image details',exact:true});await expect(inspector.getByRole('region',{name:'Positive prompt',exact:true})).toContainText('Cat portrait');
await inspector.getByRole('button',{name:'Open full preview',exact:true}).click();await expect(page.locator('dialog[open] .viewer-canvas img')).toBeVisible();
await page.keyboard.press('Escape');await expect(page.locator('dialog[open]')).toHaveCount(0);
await page.getByRole('navigation',{name:'Workspace',exact:true}).getByRole('link',{name:'Overview',exact:true}).click();await expect(page.locator('.viewer-container')).toHaveCount(0);
} finally {await host.evaluate(()=>window.libraryHost.dispose());await host.close();}
});
test('large variant histories append bounded pages without replacing the selected negative',async({page,request})=>{
await expect(await request.post('/__test__/reset',{data:{native_library:true,extra_variants:41}})).toBeOK();
await page.goto(route);await openHistory(page);
const variants=history(page).getByRole('button',{name:/^Select negative variant \d+$/});await expect(variants).toHaveCount(40);
await selectVariant(page,0);const chosenNegative=await saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre').textContent();
await history(page).getByRole('button',{name:'Load more negative variants',exact:true}).click();await expect(variants).toHaveCount(45);
await expect(history(page).getByRole('button',{name:'Select negative variant 1',exact:true})).toHaveAttribute('aria-pressed','true');
await expect(saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre')).toHaveText(chosenNegative);
});