* feat(inspector): expose bounded physical file evidence * fix(inspector): distinguish ambiguous saved associations * feat(inspector): derive conservative generation facts * fix(inspector): guard fact ancestry and inspection state * feat(inspector): share complete file details and exports * fix(inspector): preserve fact states and touch targets * feat(viewer): navigate the loaded media collection Refs #216 * fix(viewer): refit after current stage geometry changes Ref #216. Observe the current media stage after readiness and wrapped title changes without rebuilding focused controls or resetting transforms at unchanged dimensions. * feat(viewer): add lifecycle-safe slideshow and media preferences * fix(viewer): resume gallery continuation after close (#216) * fix(images): guard source recovery and wrap viewer actions Clear stale root and folder intent, cancel old pages, and wait for current source discovery before loading a refreshed collection. Preserve safe retry through failed, superseded, and disposed discovery. Wrap narrow viewer footer actions and wait for measured gallery viewport settling in responsive checks. Refs #216; addresses PR #224 review. * test(viewer): await gallery layout after viewport changes Require actual inner, root client, and document widths to agree after each responsive-case viewport and CSS zoom transition. Keep the existing exact frame, overflow, focus, and coarse target assertions unchanged. Observed PR #224 CI initial mobile resize at537px before pending card relayout settled to356px. Refs #216.
430 lines
38 KiB
JavaScript
430 lines
38 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('refresh atomically discards old row geometry and renders changed parent data',async({page,request})=>{
|
|
await page.goto(route);await expect(cards(page)).toHaveCount(3);
|
|
const card=cards(page).filter({has:page.getByText('Dog landscape',{exact:true})});const id=await card.getAttribute('data-prompt-id');
|
|
await expect(await request.put(`/prompt_manager/prompts/${id}`,{data:{text:'Revised dog landscape'}})).toBeOK();
|
|
await expect(await request.put(`/prompt_manager/prompts/${id}/rating`,{data:{rating:5}})).toBeOK();
|
|
await page.evaluate(()=>{
|
|
const native=requestAnimationFrame;const held=[];let paused=true;window.releaseLibraryFrames=()=>{paused=false;window.requestAnimationFrame=native;for(const callback of held)callback();};
|
|
window.requestAnimationFrame=callback=>native(time=>{if(paused)held.push(()=>callback(time));else callback(time);});
|
|
});
|
|
try {
|
|
await page.getByRole('button',{name:'Refresh library',exact:true}).click();
|
|
await expect(page.locator('.library-status')).not.toHaveText('Loading prompt library…');
|
|
} finally {await page.evaluate(()=>window.releaseLibraryFrames());}
|
|
await expect(cards(page)).toHaveCount(3);
|
|
const changed=page.locator(`article[data-prompt-id="${id}"]`);await expect(changed).toContainText('Revised dog landscape');
|
|
await expect(changed.getByRole('img',{name:'Rated 5 out of 5',exact:true})).toHaveCount(1);
|
|
await expect(page.getByRole('button',{name:'Retry prompt library',exact:true})).toBeHidden();
|
|
});
|
|
|
|
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('empty shelf visits restore saved scroll without inventing a parent anchor',async({page})=>{
|
|
await page.setViewportSize({width:1600,height:300});await page.goto(route);
|
|
await page.getByRole('searchbox',{name:'Search prompts',exact:true}).fill('No matching saved positive');
|
|
await expect(cards(page)).toHaveCount(0);await expect(page.locator('.library-status')).toContainText('No matching prompts.');
|
|
await page.evaluate(()=>scrollTo(0,document.documentElement.scrollHeight));
|
|
await expect.poll(()=>page.evaluate(()=>scrollY)).toBeGreaterThan(0);
|
|
const position=await page.evaluate(()=>scrollY);
|
|
await page.getByRole('navigation',{name:'Workspace',exact:true}).getByRole('link',{name:'Overview',exact:true}).evaluate(node=>node.click());
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
|
|
await page.getByRole('navigation',{name:'Workspace',exact:true}).getByRole('link',{name:'Prompt library',exact:true}).evaluate(node=>node.click());
|
|
await expect(page.getByRole('searchbox',{name:'Search prompts',exact:true})).toHaveValue('No matching saved positive');
|
|
await expect(cards(page)).toHaveCount(0);
|
|
await expect.poll(()=>page.evaluate(expected=>Math.abs(scrollY-expected),position)).toBeLessThanOrEqual(1);
|
|
const search=page.getByRole('searchbox',{name:'Search prompts',exact:true});await search.focus();await search.fill('Dog landscape');
|
|
await expect(cards(page)).toHaveCount(1);await expect(search).toBeFocused();
|
|
await page.evaluate(()=>scrollTo(0,0));await page.getByRole('button',{name:'Refresh library',exact:true}).click();await expect(cards(page)).toHaveCount(1);
|
|
expect(await page.evaluate(()=>scrollY)).toBe(0);
|
|
const anchor=await firstVisibleParent(page);expect(anchor).not.toBeNull();
|
|
await cards(page).getByRole('link',{name:'Explore history',exact:true}).evaluate(node=>node.click());await expect(history(page)).toBeVisible();
|
|
await page.setViewportSize({width:390,height:844});await page.getByRole('link',{name:'Back to library',exact:true}).click();
|
|
await retainedParent(page,anchor);await expect(page.getByRole('searchbox',{name:'Search prompts',exact:true})).toHaveValue('Dog landscape');
|
|
});
|
|
|
|
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 expect(inspector.locator('.generation-facts dt')).toHaveText(['Seed','Steps','CFG','Sampler','Scheduler','Model']);
|
|
await expect(inspector.locator('summary')).toHaveText(['File & provenance','Workflow & raw metadata']);
|
|
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);
|
|
});
|
|
|
|
async function observeLibrary(page, hold=false) {
|
|
await page.addInitScript(({hold})=>{
|
|
const NativeResizeObserver=window.ResizeObserver;
|
|
const records=[];let holding=hold;
|
|
const raf=window.requestAnimationFrame.bind(window),cancel=window.cancelAnimationFrame.bind(window);
|
|
const pending=new Set(),heldFrames=new Map();let requested=0,mutations=0,unmounted=0,holdingFrames=false;
|
|
window.requestAnimationFrame=callback=>{requested++;const id=raf(time=>{const deliver=()=>{pending.delete(id);callback(time);};if(holdingFrames)heldFrames.set(id,deliver);else deliver();});pending.add(id);return id;};
|
|
window.cancelAnimationFrame=id=>{pending.delete(id);heldFrames.delete(id);cancel(id);};
|
|
window.ResizeObserver=class {
|
|
constructor(callback) {
|
|
const record={callback,targets:new Set(),entries:new Map(),latest:new Map(),disconnected:false};
|
|
record.native=new NativeResizeObserver(entries=>{
|
|
const ready=[];
|
|
for(const entry of entries) {
|
|
if(entry.target.matches('.library-card')) {
|
|
if(!entry.target.isConnected)unmounted++;
|
|
record.latest.set(entry.target,entry);
|
|
if(holding) {record.entries.set(entry.target,entry);continue;}
|
|
}
|
|
ready.push(entry);
|
|
}
|
|
if(ready.length)callback(ready,this);
|
|
});records.push(record);this.record=record;
|
|
}
|
|
observe(target,options){this.record.targets.add(target);this.record.native.observe(target,options);}
|
|
unobserve(target){this.record.targets.delete(target);this.record.entries.delete(target);this.record.latest.delete(target);this.record.native.unobserve(target);}
|
|
disconnect(){this.record.disconnected=true;this.record.targets.clear();this.record.entries.clear();this.record.native.disconnect();}
|
|
};
|
|
new MutationObserver(entries=>{mutations+=entries.filter(entry=>entry.target.matches('.library-card,.library-row,.library-row-spacer')).length;}).observe(document,{subtree:true,attributes:true,attributeFilter:['style']});
|
|
window.libraryObservation={
|
|
hold(){holding=true;},
|
|
release(){holding=false;for(const record of records){const entries=[...record.entries.values()].filter(entry=>record.targets.has(entry.target));record.entries.clear();if(entries.length)record.callback(entries,record.native);}},
|
|
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);}},
|
|
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);
|
|
}
|
|
async function firstVisibleParent(page) {
|
|
return cards(page).evaluateAll(nodes=>{
|
|
const card=nodes.find(node=>{const box=node.getBoundingClientRect();return box.bottom>0&&box.top<innerHeight;});
|
|
return card?{id:card.dataset.promptId,offset:card.getBoundingClientRect().top}:null;
|
|
});
|
|
}
|
|
async function retainedParent(page, anchor) {
|
|
const card=page.locator(`article[data-prompt-id="${anchor.id}"]`);
|
|
await expect(card).toHaveCount(1);
|
|
await expect.poll(async()=>Math.abs(await card.evaluate(node=>node.getBoundingClientRect().top)-anchor.offset)).toBeLessThanOrEqual(2);
|
|
}
|
|
|
|
test('Library natural rows retain long prompt content without overflow',async({page,request})=>{
|
|
await request.post('/__test__/reset',{data:{workspace_visual:true}});
|
|
const data=await(await request.get('/prompt_manager/library')).json();
|
|
for(const [width,height] of [[1600,1100],[390,844]]) {
|
|
await page.setViewportSize({width,height});await page.goto(route);await expect(cards(page)).toHaveCount(6);
|
|
expect(await cards(page).filter({hasText:'A study in very long creative context'}).evaluate(node=>node.getBoundingClientRect().height)).toBeGreaterThan(560);
|
|
await expect(page.locator('.workspace-heading-copy > .intro')).toContainText('One positive.');
|
|
await expect(page.locator('.library-shelf > .intro')).toHaveCount(0);
|
|
for(const theme of ['dark','light']) {
|
|
await page.getByRole('combobox',{name:'Appearance',exact:true}).selectOption(theme);
|
|
await expect(page.locator('.prompt-positive h3').first()).toHaveCSS('color',theme==='dark'?'rgb(115, 221, 169)':'rgb(23, 107, 70)');
|
|
}
|
|
await expect(page.locator('.library-card-art').first()).toHaveCSS('height',width===1600?'170px':'150px');
|
|
await expect(page.locator('.library-card-content').first()).toHaveCSS('padding',width===1600?'18px':'15px');
|
|
await expect(page.locator('.library-grid')).toHaveCSS('gap','18px');
|
|
const long=cards(page).filter({hasText:'A study in very long creative context'});
|
|
const prompt=long.locator('.prompt-text');expect((await prompt.textContent()).length).toBeGreaterThan(1500);
|
|
expect(await long.evaluate(node=>node.getBoundingClientRect().height)).toBeGreaterThan(560);
|
|
for(const selector of ['.library-card-title','.library-card-prompts','.prompt-text','.library-card-content > .library-tags']) {
|
|
expect(await long.locator(selector).evaluate(node=>node.scrollHeight<=node.clientHeight+1)).toBe(true);
|
|
}
|
|
await prompt.focus();await expect(prompt).toBeFocused();
|
|
await long.getByRole('link',{name:'Explore history',exact:true}).scrollIntoViewIfNeeded();
|
|
await expect(long.getByRole('link',{name:'Explore history',exact:true})).toBeVisible();
|
|
for(const item of data.items) {
|
|
const card=page.locator(`article[data-prompt-id="${item.id}"]`);
|
|
await expect(card.getByRole('img',{name:item.rating?`Rated ${item.rating} out of 5`:'Unrated',exact:true})).toHaveCount(1);
|
|
await expect(card.locator('.library-card-content > .library-tags .library-chip')).toHaveText(item.tags);
|
|
}
|
|
expect(await page.evaluate(()=>document.documentElement.scrollWidth-document.documentElement.clientWidth)).toBeLessThanOrEqual(1);
|
|
await expect(page.locator('.library-shelf')).not.toContainText(/\b\d+ (?:tracked )?runs\b|Used today/i);
|
|
const boxes=await cards(page).evaluateAll(nodes=>nodes.map(node=>{const b=node.getBoundingClientRect();return {x:b.x,y:b.y,right:b.right,bottom:b.bottom};}));
|
|
for(let i=0;i<boxes.length;i++) for(const b of boxes.slice(i+1)) {const a=boxes[i];expect(a.right<=b.x+1||b.right<=a.x+1||a.bottom<=b.y+1||b.bottom<=a.y+1).toBe(true);}
|
|
}
|
|
});
|
|
|
|
test('row measurement preserves anchor after width invalidation and append',async({page,request})=>{
|
|
await request.post('/__test__/reset',{data:{native_library:true,extra_prompts:200}});
|
|
await observeLibrary(page,true);await page.addInitScript(()=>{window.IntersectionObserver=undefined;});
|
|
let release;const held=new Promise(resolve=>{release=resolve;});
|
|
await page.route('**/images/serve/**',async route=>{await held;await route.continue().catch(()=>{});});
|
|
try {
|
|
await page.setViewportSize({width:1600,height:1100});await page.goto(route);
|
|
const more=page.getByRole('button',{name:'Load more prompts',exact:true});
|
|
for(const count of [40,80,120]) {await expect(page.locator('.library-caption')).toContainText(`${count} of 203`);await more.evaluate(node=>node.click());}
|
|
await expect(page.locator('.library-caption')).toContainText('160 of 203');
|
|
await page.evaluate(()=>scrollTo(0,document.querySelector('.library-grid').getBoundingClientRect().top+scrollY+40*486+25));
|
|
await settleLibrary(page);const anchor=await firstVisibleParent(page);expect(anchor).not.toBeNull();
|
|
const pinned=cards(page).first();const pinnedId=await pinned.getAttribute('data-prompt-id');await pinned.locator('a').first().evaluate(node=>node.focus({preventScroll:true}));
|
|
await page.evaluate(()=>window.libraryObservation.release());await settleLibrary(page);await retainedParent(page,anchor);
|
|
expect(await cards(page).count()).toBeLessThan(80);await expect(page.locator(`article[data-prompt-id="${pinnedId}"] a`).first()).toBeFocused();
|
|
const after=await firstVisibleParent(page);release();await page.evaluate(async()=>{await Promise.all([...document.querySelectorAll('.library-card img')].map(image=>image.decode().catch(()=>{})));});await settleLibrary(page);await retainedParent(page,after);
|
|
await more.evaluate(node=>node.click());await expect(page.locator('.library-caption')).toContainText('200 of 203');await settleLibrary(page);await retainedParent(page,after);
|
|
await expect(page.locator(`article[data-prompt-id="${pinnedId}"] a`).first()).toBeFocused();
|
|
await page.setViewportSize({width:390,height:844});await settleLibrary(page);await retainedParent(page,after);
|
|
await expect(page.locator(`article[data-prompt-id="${pinnedId}"] a`).first()).toBeFocused();
|
|
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);
|
|
for(const changedWidth of [false,true]) {
|
|
const before=await firstVisibleParent(page);
|
|
await page.locator(`article[data-prompt-id="${before.id}"] .library-history-link`).evaluate(node=>node.click());await expect(history(page)).toBeVisible();
|
|
if(changedWidth)await page.setViewportSize({width:1600,height:1100});
|
|
await page.getByRole('link',{name:'Back to library',exact:true}).click();await settleLibrary(page);await retainedParent(page,before);
|
|
expect(await cards(page).count()).toBeLessThan(80);
|
|
}
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).unmounted).toBe(0);
|
|
} finally {release();}
|
|
});
|
|
|
|
test('queued row growth preserves newer native wheel movement and correction events',async({page,request},testInfo)=>{
|
|
await request.post('/__test__/reset',{data:{native_library:true,extra_prompts:200}});
|
|
await observeLibrary(page);await page.addInitScript(()=>{window.IntersectionObserver=undefined;});
|
|
await page.setViewportSize({width:1600,height:1100});await page.goto(route);
|
|
const more=page.getByRole('button',{name:'Load more prompts',exact:true});
|
|
for(const count of [40,80,120]) {await expect(page.locator('.library-caption')).toContainText(`${count} of 203`);await more.evaluate(node=>node.click());}
|
|
await expect(page.locator('.library-caption')).toContainText('160 of 203');
|
|
await page.evaluate(()=>scrollTo(0,document.querySelector('.library-grid').getBoundingClientRect().top+scrollY+20*486+25));
|
|
await settleLibrary(page);await page.mouse.move(1100,550);
|
|
const pinned=cards(page).first(),pinnedId=await pinned.getAttribute('data-prompt-id');await pinned.locator('a').first().evaluate(node=>node.focus({preventScroll:true}));
|
|
try {
|
|
// The same queued-frame wheel sequence is stable without changed measurements.
|
|
const control=await firstVisibleParent(page),controlY=await page.evaluate(()=>scrollY);
|
|
await page.evaluate(()=>window.libraryObservation.holdFrames());await page.mouse.wheel(0,160);
|
|
await expect.poll(()=>page.evaluate(()=>scrollY)).toBe(controlY+160);
|
|
await expect.poll(()=>page.evaluate(()=>window.libraryObservation.stats().heldFrames)).toBeGreaterThan(0);
|
|
await page.evaluate(()=>window.libraryObservation.releaseFrames());await settleLibrary(page);
|
|
expect(await page.evaluate(()=>scrollY)).toBe(controlY+160);await retainedParent(page,{...control,offset:control.offset-160});
|
|
|
|
const before=await firstVisibleParent(page),beforeY=await page.evaluate(()=>scrollY);
|
|
await page.evaluate(()=>{window.libraryObservation.hold();for(const content of document.querySelectorAll('.library-card-content'))content.style.paddingBlockEnd='45px';});
|
|
await expect.poll(()=>page.evaluate(()=>window.libraryObservation.stats().measurements)).toBeGreaterThan(0);
|
|
const parent=page.locator(`article[data-prompt-id="${before.id}"]`);
|
|
const growth=await parent.evaluate(node=>node.getBoundingClientRect().top)-before.offset;
|
|
expect(growth).toBeGreaterThan(0);expect(await page.evaluate(()=>scrollY)).toBe(beforeY);
|
|
await page.evaluate(()=>{window.libraryObservation.holdFrames();window.libraryObservation.release();});
|
|
await expect.poll(()=>page.evaluate(()=>window.libraryObservation.stats().heldFrames)).toBeGreaterThan(0);
|
|
await page.mouse.wheel(0,160);await expect.poll(()=>page.evaluate(()=>scrollY)).toBe(beforeY+160);
|
|
expect(await page.evaluate(()=>document.documentElement.scrollHeight-innerHeight-scrollY)).toBeGreaterThan(1000);
|
|
await page.evaluate(()=>window.libraryObservation.releaseFrames());await settleLibrary(page);
|
|
await testInfo.attach('queued-growth-wheel-geometry',{body:JSON.stringify({beforeY,offset:before.offset,growth,wheel:160,finalY:await page.evaluate(()=>scrollY),finalOffset:await parent.evaluate(node=>node.getBoundingClientRect().top)}),contentType:'application/json'});
|
|
await retainedParent(page,{...before,offset:before.offset-160});
|
|
expect(Math.abs(await page.evaluate(()=>scrollY)-(beforeY+160+growth))).toBeLessThanOrEqual(2);
|
|
|
|
// The correction's own native scroll event must not become new user motion.
|
|
const corrected=await firstVisibleParent(page);
|
|
await page.evaluate(()=>{window.libraryObservation.hold();for(const content of document.querySelectorAll('.library-card-content'))content.style.paddingBlockEnd='65px';});
|
|
await expect.poll(()=>page.evaluate(()=>window.libraryObservation.stats().measurements)).toBeGreaterThan(0);
|
|
await page.evaluate(()=>window.libraryObservation.release());await settleLibrary(page);await retainedParent(page,corrected);
|
|
await expect(page.locator(`article[data-prompt-id="${pinnedId}"] a`).first()).toBeFocused();
|
|
const ids=await cards(page).evaluateAll(nodes=>nodes.map(node=>node.dataset.promptId));expect(ids.length).toBeLessThan(80);expect(new Set(ids).size).toBe(ids.length);
|
|
await page.evaluate(()=>window.libraryObservation.holdFrames());await page.mouse.wheel(0,160);
|
|
await expect.poll(()=>page.evaluate(()=>window.libraryObservation.stats().heldFrames)).toBeGreaterThan(0);
|
|
await page.evaluate(()=>{location.hash='#/overview';});await expect(cards(page)).toHaveCount(0);
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).targets).toBe(0);
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).pending).toBe(0);
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).heldFrames).toBe(0);
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).unmounted).toBe(0);
|
|
} finally {await page.evaluate(()=>{window.libraryObservation.release();window.libraryObservation.releaseFrames();});}
|
|
});
|
|
|
|
test('unchanged row observations settle without resize feedback',async({page,request,browserName})=>{
|
|
await request.post('/__test__/reset',{data:{native_library:true,extra_prompts:200}});
|
|
await observeLibrary(page);await page.addInitScript(()=>{window.IntersectionObserver=undefined;});
|
|
let session,breakpoint;
|
|
try {
|
|
if(browserName==='chromium') {
|
|
session=await page.context().newCDPSession(page);await session.send('Debugger.enable');
|
|
await page.addInitScript(()=>{window.libraryLayoutCalls=0;});
|
|
const scripts=[];session.on('Debugger.scriptParsed',script=>scripts.push(script));
|
|
await page.goto(route);await expect(cards(page).first()).toBeVisible();await settleLibrary(page);
|
|
const script=scripts.find(script=>script.url.endsWith('/workspace/library-layout.mjs'));expect(script).toBeTruthy();
|
|
const {scriptSource}=await session.send('Debugger.getScriptSource',{scriptId:script.scriptId});
|
|
const lines=scriptSource.split('\n');const start=lines.findIndex(line=>line.includes('export function layoutLibraryRows('));expect(start).toBeGreaterThanOrEqual(0);
|
|
const possible=await session.send('Debugger.getPossibleBreakpoints',{start:{scriptId:script.scriptId,lineNumber:start},end:{scriptId:script.scriptId,lineNumber:start+3},restrictToFunction:false});
|
|
const location=possible.locations.find(location=>location.lineNumber>start);expect(location).toBeTruthy();
|
|
breakpoint=(await session.send('Debugger.setBreakpoint',{location,condition:'(window.libraryLayoutCalls++, false)'})).breakpointId;
|
|
} else {await page.goto(route);await expect(cards(page).first()).toBeVisible();await settleLibrary(page);}
|
|
const more=page.getByRole('button',{name:'Load more prompts',exact:true});
|
|
for(const count of [40,80,120]) {await expect(page.locator('.library-caption')).toContainText(`${count} of 203`);await more.evaluate(node=>node.click());}
|
|
await expect(page.locator('.library-caption')).toContainText('160 of 203');await settleLibrary(page);
|
|
if(session)expect(await page.evaluate(()=>window.libraryLayoutCalls)).toBeGreaterThan(0);
|
|
const before=await page.evaluate(()=>({stats:window.libraryObservation.stats(),calls:window.libraryLayoutCalls}));
|
|
await page.evaluate(()=>{for(let i=0;i<12;i++){window.libraryObservation.equal();dispatchEvent(new Event('scroll'));}});await settleLibrary(page);
|
|
const after=await page.evaluate(()=>({stats:window.libraryObservation.stats(),calls:window.libraryLayoutCalls}));
|
|
expect(after.stats.requested-before.stats.requested).toBeLessThanOrEqual(1);expect(after.stats.mutations).toBe(before.stats.mutations);if(session)expect(after.calls).toBe(before.calls);
|
|
for(let i=0;i<3;i++) {await page.evaluate(()=>scrollBy(0,1));await settleLibrary(page);}
|
|
if(session)expect(await page.evaluate(()=>window.libraryLayoutCalls)).toBe(after.calls);
|
|
const equalBefore=await page.evaluate(()=>window.libraryObservation.stats().requested);await page.evaluate(()=>{for(let i=0;i<12;i++)window.libraryObservation.equal();});await settleLibrary(page);
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).requested).toBe(equalBefore);
|
|
await page.evaluate(()=>{window.libraryObservation.hold();for(const content of document.querySelectorAll('.library-card-content'))content.style.paddingBlockEnd='35px';});await settleLibrary(page);
|
|
const calls=await page.evaluate(()=>window.libraryLayoutCalls);await page.evaluate(()=>window.libraryObservation.release());await settleLibrary(page);
|
|
if(session)expect(await page.evaluate(()=>window.libraryLayoutCalls)-calls).toBe(1);
|
|
await page.evaluate(()=>{dispatchEvent(new Event('scroll'));location.hash='#/overview';});await expect(cards(page)).toHaveCount(0);await settleLibrary(page);
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).targets).toBe(0);
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).pending).toBe(0);
|
|
expect((await page.evaluate(()=>window.libraryObservation.stats())).unmounted).toBe(0);
|
|
} finally {if(breakpoint)await session.send('Debugger.removeBreakpoint',{breakpointId:breakpoint});if(session)await session.detach();}
|
|
});
|