feat(workspace): concept prompt blocks
Shared prompt blocks (Images panel, viewer details, Library cards and saved texts, recorded runs) now render the approved mock's markup (app.js promptBlock a298-302, concept.css s550-588), #249 D22: - section.prompt-section.<role>[aria-label="<Role> prompt"] > .prompt-heading (span.role-label "+ Positive prompt" / "- Negative prompt"; .prompt-actions: copy button.icon-btn "Copy <role> prompt", then send button.send.icon-btn "Send <role> prompt to ComfyUI") + p.prompt-text. - No per-block source line; a file's evidence moves to the panel badge (D2) and "File & provenance" (D3). Recorded runs keep every field's source in "Frozen field provenance". - Empty text reads "No negative prompt (explicitly empty)" and still sends an explicit clear. Absent, ambiguous and unresolved roles render p.prompt-text.muted with the existing status strings and no actions; unknown role names render nothing and cannot be sent or copied. - Copy shows the shared toast "Prompt copied."; without a clipboard it opens the concept's "Copy prompt" select-to-copy dialog. - The data-send-role / data-copy-role contract is unchanged. The D22 interim overrides (26px actions, prompt-text margins, copy status, source line) are retired so the concept's rules apply; the action buttons measure the mock's 31x31. pre.prompt-text styling stays only for the Library editor's Archive dialog and frozen field provenance, which keep their own markup until their page rebuilds. Test selector mapping: .prompt-block / .prompt-positive -> .prompt-section(.positive) .prompt-block-heading h3 -> .prompt-heading span.role-label region pre / pre.prompt-text -> p.prompt-text "Empty negative prompt" -> "No negative prompt (explicitly empty)" "Send <role> prompt" -> "Send <role> prompt to ComfyUI" .prompt-source / copy status line -> removed (toast or Copy prompt dialog) copy button 30x26 -> 31x31 (measured in the mock)
This commit is contained in:
@@ -107,7 +107,8 @@ test('full inspector shows six facts and scoped provenance',async({page,request}
|
||||
expect(await panel.evaluate(node=>node.getBoundingClientRect().width)).toBe(310);
|
||||
expect(await panel.locator('.file-inspector-inner').evaluate(node=>getComputedStyle(node).padding)).toBe('17px');
|
||||
if (!await page.evaluate(()=>matchMedia('(pointer: coarse)').matches)) {
|
||||
const copy=await panel.getByRole('button',{name:'Copy positive prompt',exact:true}).boundingBox();expect(copy.width).toBe(30);expect(copy.height).toBe(26);
|
||||
// The concept's prompt actions (`.prompt-actions button.icon-btn`) measure 31×31 in the mock.
|
||||
const copy=await panel.getByRole('button',{name:'Copy positive prompt',exact:true}).boundingBox();expect(copy.width).toBe(31);expect(copy.height).toBe(31);
|
||||
expect((await panel.getByRole('button',{name:'Send prompt pair',exact:true}).boundingBox()).height).toBe(30);
|
||||
}
|
||||
await expect(panel.locator('.generation-facts dt')).toHaveText(['Seed','Steps','CFG','Sampler','Scheduler','Model']);
|
||||
@@ -124,8 +125,8 @@ test('full inspector shows six facts and scoped provenance',async({page,request}
|
||||
}
|
||||
await expect(panel).not.toContainText(/Run:|Original file unchanged|Execution recorded/);
|
||||
await selectFile(page,'empty.png');await expect(panel.locator('.file-format')).toHaveText('PNG');await expect(panel.getByRole('heading',{name:'empty.png',exact:true})).toBeVisible();
|
||||
await expect(panel.getByRole('button',{name:'Send negative prompt',exact:true})).toHaveCount(0);await expect(panel).toContainText('Not recorded in this file.');await expect(panel.getByRole('link',{name:'Prompt history',exact:true})).toHaveCount(0);
|
||||
await selectFile(page,'explicit-empty.png');await expect(panel).toContainText('Empty negative prompt');await expect(panel.getByRole('button',{name:'Send prompt pair',exact:true})).toHaveCount(1);
|
||||
await expect(panel.getByRole('button',{name:'Send negative prompt to ComfyUI',exact:true})).toHaveCount(0);await expect(panel).toContainText('Not recorded in this file.');await expect(panel.getByRole('link',{name:'Prompt history',exact:true})).toHaveCount(0);
|
||||
await selectFile(page,'explicit-empty.png');await expect(panel.locator('.prompt-section.negative p.prompt-text')).toHaveText('No negative prompt (explicitly empty)');await expect(panel.getByRole('button',{name:'Send prompt pair',exact:true})).toHaveCount(1);
|
||||
await selectFile(page,'workflow-only.png');await panel.getByText('File & provenance',{exact:true}).click();
|
||||
await expect(panel.getByText('Seed: Configuration evidence · Saved workflow configuration',{exact:true})).toBeVisible();
|
||||
if (testInfo.project.name === 'chromium') {
|
||||
@@ -138,7 +139,7 @@ test('full inspector preserves long role text and exact sends',async({page,reque
|
||||
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',label:'Exact pair',type:'PromptManagerPair',roles:['positive','negative']};window.sent=[];window.bridge=createBridgeHost({origin:location.origin,namespace:'/',label:'Exact host',adapter:{listTargets:()=>[target],preview:(_id,roles)=>({preview:{target,fields:Object.entries(roles).map(([role,after])=>({role,before:'',after}))},snapshot:roles}),apply:roles=>{window.sent.push(roles);return{targetId:target.id,values:roles};}}});});
|
||||
try {
|
||||
await fullFixture(page,request);const panel=inspector(page);const positive=panel.locator('.prompt-positive .prompt-text');await expect(positive).toContainText('猫 😀');const text=await positive.textContent();expect(text.length).toBeGreaterThan(1000);
|
||||
await fullFixture(page,request);const panel=inspector(page);const positive=panel.locator('.prompt-section.positive .prompt-text');await expect(positive).toContainText('猫 😀');const text=await positive.textContent();expect(text.length).toBeGreaterThan(1000);
|
||||
await panel.getByRole('button',{name:'Send prompt pair',exact:true}).click();const sender=page.getByRole('dialog',{name:'Send to ComfyUI',exact:true});
|
||||
await expect(sender.getByRole('combobox',{name:'ComfyUI window',exact:true}).locator('option')).toContainText(['Exact host']);
|
||||
await sender.getByRole('combobox',{name:'ComfyUI window',exact:true}).selectOption({label:'Exact host'});await sender.getByRole('combobox',{name:'Destination node',exact:true}).selectOption({label:'Exact pair'});
|
||||
@@ -190,7 +191,7 @@ test('full inspector keeps long literal content reachable on mobile',async({page
|
||||
const literal='<svg onload="window.executed=true"><image href="https://invalid.test/never"/></svg></pre>'+'verylongmodelpath/'.repeat(30);
|
||||
let external=0;await page.route('https://invalid.test/**',route=>{external++;return route.abort();});
|
||||
await page.route('**/images/inspect/**',async route=>{const response=await route.fetch();const data=await response.json();data.metadata.raw.parameters=literal;data.metadata.record_formats.parameters='original-text';data.metadata.parameters=literal;data.metadata.prompt_graph.value['4'].inputs.ckpt_name=literal;await route.fulfill({response,json:data});});
|
||||
await page.setViewportSize({width:390,height:844});await fullFixture(page,request);const panel=inspector(page);await expect(panel.locator('.prompt-positive pre')).toContainText('fine geometry and light.');await panel.getByRole('button',{name:'Copy positive prompt',exact:true}).click();await expect(panel).toContainText('Copy unavailable. Select the prompt text to copy it.');
|
||||
await page.setViewportSize({width:390,height:844});await fullFixture(page,request);const panel=inspector(page);await expect(panel.locator('.prompt-section.positive p.prompt-text')).toContainText('fine geometry and light.');await panel.getByRole('button',{name:'Copy positive prompt',exact:true}).click();const fallback=page.getByRole('dialog',{name:'Copy prompt',exact:true});await expect(fallback.getByRole('textbox')).toHaveValue(/^Inspector portrait 猫 😀\nFull prompt context, fine geometry and light\./);await page.keyboard.press('Escape');await expect(fallback).toBeHidden();
|
||||
await panel.getByText('Workflow & raw metadata',{exact:true}).click();await expect(panel.locator('pre code').filter({hasText:literal}).last()).toHaveText(literal);await expect(panel.locator('.generation-facts dd').last()).toHaveText(literal);
|
||||
for(const zoom of [1,2]) {await page.evaluate(zoom=>{document.documentElement.style.zoom=String(zoom);},zoom);await expect.poll(()=>page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true);const footer=panel.getByRole('button',{name:'Send prompt pair',exact:true});await footer.focus();await expect(footer).toBeFocused();const summary=panel.getByText('File & provenance',{exact:true});await summary.focus();await page.keyboard.press('Enter');await expect(panel).toContainText('historical bytes are not verified');}
|
||||
expect(await page.evaluate(()=>window.executed)).toBeUndefined();expect(external).toBe(0);
|
||||
@@ -252,8 +253,8 @@ test.describe('full inspector coarse pointer',()=>{
|
||||
test.use({hasTouch:true,viewport:{width:390,height:844}});
|
||||
test('full inspector meets coarse target sizes and keeps long content reachable at zoom',async({page,request})=>{
|
||||
await fullFixture(page,request);expect(await page.evaluate(()=>matchMedia('(pointer: coarse)').matches)).toBe(true);const panel=inspector(page);
|
||||
await expect(panel.locator('.prompt-positive pre')).toContainText('Full prompt context, fine geometry and light.');
|
||||
const fullText=await panel.locator('.prompt-positive pre').textContent();expect(fullText.length).toBeGreaterThan(1000);
|
||||
await expect(panel.locator('.prompt-section.positive p.prompt-text')).toContainText('Full prompt context, fine geometry and light.');
|
||||
const fullText=await panel.locator('.prompt-section.positive p.prompt-text').textContent();expect(fullText.length).toBeGreaterThan(1000);
|
||||
await panel.getByText('File & provenance',{exact:true}).click();await panel.getByText('Workflow & raw metadata',{exact:true}).click();
|
||||
const bounds=await panel.locator('button,a,summary').evaluateAll(nodes=>nodes.map(node=>({name:node.getAttribute('aria-label')||node.textContent,role:node.matches('.prompt-actions button'),width:node.getBoundingClientRect().width,height:node.getBoundingClientRect().height})));
|
||||
for(const target of bounds){expect(target.width,target.name).toBeGreaterThanOrEqual(target.role?36:40);expect(target.height,target.name).toBeGreaterThanOrEqual(target.role?36:40);}
|
||||
@@ -264,7 +265,7 @@ test.describe('full inspector coarse pointer',()=>{
|
||||
for(const target of [panel.locator('summary').first(),panel.getByRole('button',{name:'Download metadata JSON',exact:true}),panel.getByRole('link',{name:'Prompt history',exact:true})]) {
|
||||
await target.scrollIntoViewIfNeeded();await target.focus();await expect(target).toBeFocused();await expect(target).toBeInViewport();
|
||||
}
|
||||
await expect(panel.locator('.prompt-positive pre')).toHaveText(fullText);
|
||||
await expect(panel.locator('.prompt-section.positive p.prompt-text')).toHaveText(fullText);
|
||||
}
|
||||
await panel.getByRole('button',{name:'Open full preview',exact:true}).click();const viewer=page.locator('dialog#viewer-dialog');
|
||||
await expect(viewer.locator('.viewer-canvas img')).toBeVisible();
|
||||
@@ -525,9 +526,11 @@ test('slideshow production query and route disposal cancel the active deadline',
|
||||
});
|
||||
|
||||
test('slideshow interaction visibility sender and disposal clear intent',async({page,request})=>{
|
||||
// A working clipboard keeps the copy action on the toast, so no "Copy prompt" fallback covers the viewer.
|
||||
await page.addInitScript(()=>Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async()=>{}}}));
|
||||
await slideshowFixture(page,request);const button=slideshowButton(page),modal=mediaDialog(page);
|
||||
for(const action of [
|
||||
()=>modal.locator('.prompt-positive pre').click(),
|
||||
()=>modal.locator('.prompt-section.positive p.prompt-text').click(),
|
||||
()=>modal.locator('#viewer-details').dispatchEvent('wheel',{deltaY:100}),
|
||||
()=>modal.getByRole('button',{name:'Copy positive prompt',exact:true}).click(),
|
||||
async()=>{await modal.getByText('Workflow & raw metadata',{exact:true}).click();await downloadText(page,modal.getByRole('button',{name:'Download metadata JSON',exact:true}));},
|
||||
@@ -592,7 +595,7 @@ test('full inspector attaches matched theme and mobile evidence with reachable c
|
||||
await openCollection(page);const modal=mediaDialog(page),rail=modal.locator('#viewer-details');
|
||||
await expect(modal.locator('.viewer-canvas img')).toBeVisible();await expect.poll(()=>modal.locator('.viewer-canvas img').evaluate(node=>node.complete&&node.naturalWidth>0)).toBe(true);
|
||||
await modal.evaluate(node=>{node.scrollTop=0;});await capture(`viewer-${label}`,true);
|
||||
await expect(rail.locator('.prompt-negative')).toContainText('blurred edges, watermark');await rail.locator('.generation-facts').scrollIntoViewIfNeeded();await capture(`facts-${label}`,true);
|
||||
await expect(rail.locator('.prompt-section.negative')).toContainText('blurred edges, watermark');await rail.locator('.generation-facts').scrollIntoViewIfNeeded();await capture(`facts-${label}`,true);
|
||||
await rail.getByText('File & provenance',{exact:true}).click();await rail.locator('.file-facts').scrollIntoViewIfNeeded();await capture(`provenance-${label}`,true);
|
||||
await rail.getByText('Workflow & raw metadata',{exact:true}).click();const raw=rail.locator('pre code').first();await raw.scrollIntoViewIfNeeded();await expect(raw).toBeInViewport();await capture(`raw-${label}`,true);
|
||||
const send=rail.getByRole('button',{name:'Send prompt pair',exact:true});await send.focus();await expect(send).toBeInViewport();await send.click();await expect(page.getByRole('dialog',{name:'Send to ComfyUI',exact:true})).toBeVisible();await capture(`sender-${label}`);await page.keyboard.press('Escape');await expect(send).toBeFocused();
|
||||
@@ -640,7 +643,7 @@ test('collection viewer keeps nested dialog focus and route cleanup',async({page
|
||||
const send=modal.getByRole('button',{name:'Send prompt pair',exact:true});await send.click();const sender=page.getByRole('dialog',{name:'Send to ComfyUI',exact:true});
|
||||
await expect(sender).toBeVisible();const before=await position(page).textContent();await page.keyboard.press('ArrowRight');await expect(position(page)).toHaveText(before);
|
||||
await page.keyboard.press('Escape');await expect(sender).toBeHidden();await expect(modal).toBeVisible();await expect(send).toBeFocused();
|
||||
const text=modal.locator('.prompt-positive pre');await text.focus();await page.keyboard.press('ArrowRight');await expect(position(page)).toHaveText(before);
|
||||
const text=modal.locator('.prompt-section.positive p.prompt-text');await text.focus();await page.keyboard.press('ArrowRight');await expect(position(page)).toHaveText(before);
|
||||
await modal.getByText('Workflow & raw metadata',{exact:true}).focus();await page.keyboard.press('ArrowLeft');await expect(position(page)).toHaveText(before);
|
||||
await modal.getByRole('button',{name:'Next media',exact:true}).click();const key=(await position(page).textContent()).split('loaded: ')[1];
|
||||
await page.keyboard.press('Escape');await expect(modal).toHaveCount(0);await expect(page.locator('.media-card.selected .image-open')).toBeFocused();await expect(inspector(page).locator('.file-name')).toHaveText(key);
|
||||
|
||||
@@ -38,8 +38,8 @@ export async function historyGeometry(page, production) {
|
||||
surface: '.library-surface', timeline: '.library-branches', positive: '.library-positive-lane',
|
||||
branch: '.library-variant-lane', label: '.library-variant-lane span', dot: '.library-variant-dot[aria-pressed="true"]',
|
||||
positiveLabel: '.library-positive-lane span',
|
||||
role: '[aria-label="Saved prompt texts"] .prompt-block',
|
||||
prompt: '[aria-label="Saved prompt texts"] pre', gallery: '.library-shared-gallery',
|
||||
role: '[aria-label="Saved prompt texts"] .prompt-section',
|
||||
prompt: '[aria-label="Saved prompt texts"] .prompt-text', gallery: '.library-shared-gallery',
|
||||
heading: '.library-variants h2', intro: '.library-variants > p.muted',
|
||||
footer: '.library-variant-footer', all: '.library-variants button:last-child',
|
||||
recordedPositiveLabel: '.history-positive-lane span', recordedBranch: '.history-context-branch',
|
||||
|
||||
@@ -74,10 +74,10 @@ test('physical image inspector resolves positive and negative metadata separatel
|
||||
|
||||
test('explicit empty negative is sendable while unrecorded text is never invented',async({page})=>{
|
||||
await page.goto(workspace);await select(page,'0:empty-negative.png');
|
||||
await expect(role(details(page),'Negative')).toContainText('Empty negative prompt');
|
||||
await expect(details(page).getByRole('button',{name:'Send negative prompt',exact:true})).toBeEnabled();
|
||||
await expect(role(details(page),'Negative').locator('p.prompt-text')).toHaveText('No negative prompt (explicitly empty)');
|
||||
await expect(details(page).getByRole('button',{name:'Send negative prompt to ComfyUI',exact:true})).toBeEnabled();
|
||||
await select(page,'0:uncertain.png');await expect(details(page)).toContainText('Not recorded in this file.');
|
||||
await expect(details(page).getByRole('button',{name:/^Send (positive|negative) prompt$/})).toHaveCount(0);
|
||||
await expect(details(page).getByRole('button',{name:/^Send (positive|negative) prompt to ComfyUI$/})).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('inspection failures retry and a delayed previous selection cannot replace the current prompt',async({page})=>{
|
||||
@@ -157,7 +157,7 @@ test('ComfyUI ↗ opens the pair send for a selected resolved image, else follow
|
||||
test('host apply rejection retains the current text and requires a new preview',async({page,context})=>{
|
||||
const host=await fakeHost(context,page,{rejectApply:true});
|
||||
try {
|
||||
await page.goto(workspace);await select(page,'0:shared/same.png');await details(page).getByRole('button',{name:'Send positive prompt',exact:true}).click();
|
||||
await page.goto(workspace);await select(page,'0:shared/same.png');await details(page).getByRole('button',{name:'Send positive prompt to ComfyUI',exact:true}).click();
|
||||
const sender=await chooseDestination(page);await sender.getByRole('button',{name:'Preview changes',exact:true}).click();
|
||||
await sender.getByRole('button',{name:'Apply to ComfyUI',exact:true}).click();await expect(sender).toContainText(/Workflow changed/);
|
||||
await expect(sender.getByRole('button',{name:'Apply to ComfyUI',exact:true})).toBeDisabled();
|
||||
@@ -169,7 +169,7 @@ test('nested viewer sender Escape restores focus without closing the image and n
|
||||
await page.goto(workspace);await select(page,'0:shared/same.png');
|
||||
await details(page).getByRole('button',{name:'Open full preview',exact:true}).click();
|
||||
const viewer=page.locator('dialog[open]').filter({has:page.locator('.viewer-canvas')});await expect(viewer).toBeVisible();
|
||||
const send=viewer.getByRole('button',{name:'Send positive prompt',exact:true});await send.click();await expect(dialog(page)).toBeVisible();
|
||||
const send=viewer.getByRole('button',{name:'Send positive prompt to ComfyUI',exact:true});await send.click();await expect(dialog(page)).toBeVisible();
|
||||
await page.keyboard.press('Escape');await expect(dialog(page)).toBeHidden();await expect(viewer).toBeVisible();await expect(send).toBeFocused();
|
||||
await send.click();await page.evaluate(()=>{location.hash='#/overview';});await expect(page.locator('dialog[open]')).toHaveCount(0);
|
||||
});
|
||||
@@ -178,7 +178,7 @@ test('empty negative send clears only the negative destination and preserves its
|
||||
const host=await fakeHost(context,page);
|
||||
try {
|
||||
await page.goto(workspace);await select(page,'0:empty-negative.png');
|
||||
await details(page).getByRole('button',{name:'Send negative prompt',exact:true}).click();
|
||||
await details(page).getByRole('button',{name:'Send negative prompt to ComfyUI',exact:true}).click();
|
||||
const sender=await chooseDestination(page);await sender.getByRole('button',{name:'Preview changes',exact:true}).click();
|
||||
await sender.getByRole('button',{name:'Apply to ComfyUI',exact:true}).click();await expect(sender).toContainText('Sent to ComfyUI.');
|
||||
expect(await host.evaluate(()=>window.hostValues)).toEqual({positive:'Existing positive',negative:''});
|
||||
@@ -187,7 +187,7 @@ test('empty negative send clears only the negative destination and preserves its
|
||||
|
||||
test('no open ComfyUI window is actionable and rediscovery finds a newly opened host',async({page,context})=>{
|
||||
await page.goto(workspace);await select(page,'0:shared/same.png');
|
||||
await details(page).getByRole('button',{name:'Send positive prompt',exact:true}).click();
|
||||
await details(page).getByRole('button',{name:'Send positive prompt to ComfyUI',exact:true}).click();
|
||||
const sender=dialog(page);await expect(sender).toContainText('No compatible ComfyUI windows found.');
|
||||
const host=await fakeHost(context,page);
|
||||
try {
|
||||
@@ -203,12 +203,13 @@ test('mobile inspector exposes full long prompts, truthful uncertainty and copy
|
||||
await page.addInitScript(()=>Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async()=>{throw new Error('Permission denied');}}}));
|
||||
await page.route('**/images/inspect/**',route=>route.fulfill({json:{success:true,root_scope:new URL(route.request().url()).searchParams.get('root_scope'),image:{root_index:0,path:'shared/same.png',filename:'same.png'},prompts:{positive:{status:'resolved',text,source:'execution'},negative:{status:'ambiguous',text:null,source:null}}}}));
|
||||
await page.setViewportSize({width:390,height:844});await page.goto(workspace);await select(page,'0:shared/same.png');
|
||||
const positive=role(details(page),'Positive');await expect(positive.locator('pre')).toHaveText(text);
|
||||
const positive=role(details(page),'Positive');await expect(positive.locator('p.prompt-text')).toHaveText(text);
|
||||
await expect(role(details(page),'Negative')).toContainText('Multiple possible prompts. Source could not be determined.');
|
||||
await expect(details(page).getByRole('button',{name:'Send negative prompt',exact:true})).toHaveCount(0);
|
||||
await positive.getByRole('button',{name:'Copy positive prompt',exact:true}).click();await expect(positive).toContainText('Copy unavailable. Select the prompt text to copy it.');
|
||||
await expect(details(page).getByRole('button',{name:'Send negative prompt to ComfyUI',exact:true})).toHaveCount(0);
|
||||
await positive.getByRole('button',{name:'Copy positive prompt',exact:true}).click();await expect(page.getByRole('dialog',{name:'Copy prompt',exact:true})).toBeVisible();
|
||||
await page.getByRole('button',{name:'Close dialog',exact:true}).click();
|
||||
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true);
|
||||
await positive.getByRole('button',{name:'Send positive prompt',exact:true}).click();
|
||||
await positive.getByRole('button',{name:'Send positive prompt to ComfyUI',exact:true}).click();
|
||||
await expect(dialog(page)).toBeVisible();const bounds=await dialog(page).boundingBox();
|
||||
expect(bounds.x).toBeGreaterThanOrEqual(0);expect(bounds.x+bounds.width).toBeLessThanOrEqual(391);
|
||||
});
|
||||
@@ -277,20 +278,37 @@ test('unresolved roles name the blocking node as text and offer no prompt action
|
||||
await expect(details(page).getByRole('button',{name:/^(Send|Copy) /})).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('prompt role headings contain compact real actions and honest source evidence',async({page})=>{
|
||||
test('prompt role headings contain the concept label and compact real actions without a source line',async({page})=>{
|
||||
await page.goto(workspace);await select(page,'0:shared/same.png');
|
||||
for (const name of ['Positive','Negative']) {
|
||||
const block=role(details(page),name),heading=block.locator('.prompt-block-heading');
|
||||
await expect(heading.getByRole('heading',{name:`${name} prompt`,exact:true})).toBeVisible();
|
||||
await expect(heading.getByRole('button',{name:`Copy ${name.toLowerCase()} prompt`,exact:true})).toBeVisible();
|
||||
await expect(heading.getByRole('button',{name:`Send ${name.toLowerCase()} prompt`,exact:true})).toBeVisible();
|
||||
for (const [name,label] of [['Positive','+ Positive prompt'],['Negative','\u2212 Negative prompt']]) {
|
||||
const block=role(details(page),name),heading=block.locator('.prompt-heading');
|
||||
await expect(block).toHaveClass(new RegExp(`^prompt-section ${name.toLowerCase()}$`));
|
||||
await expect(heading.locator('span.role-label')).toHaveText(label);
|
||||
await expect(heading.locator('.prompt-actions button.icon-btn').first()).toHaveAccessibleName(`Copy ${name.toLowerCase()} prompt`);
|
||||
await expect(heading.locator('.prompt-actions button.send.icon-btn')).toHaveAccessibleName(`Send ${name.toLowerCase()} prompt to ComfyUI`);
|
||||
await expect(heading.locator('[data-copy-role] svg')).toHaveCount(1);
|
||||
expect(await heading.evaluate(node=>node.nextElementSibling.classList.contains('prompt-source'))).toBe(true);
|
||||
expect(await heading.evaluate(node=>node.nextElementSibling.matches('p.prompt-text')&&!node.nextElementSibling.nextElementSibling)).toBe(true);
|
||||
}
|
||||
await expect(role(details(page),'Positive').locator('.prompt-source')).toHaveText('Inferred from the saved workflow');
|
||||
await expect(details(page)).not.toContainText('Inferred from the saved workflow');
|
||||
await select(page,'0:empty-negative.png');
|
||||
await expect(role(details(page),'Negative')).toContainText('Empty negative prompt');
|
||||
await expect(role(details(page),'Negative').locator('.prompt-block-heading [data-copy-role]')).toHaveCount(1);
|
||||
await expect(role(details(page),'Negative').locator('p.prompt-text')).toHaveText('No negative prompt (explicitly empty)');
|
||||
await expect(role(details(page),'Negative').locator('.prompt-heading [data-copy-role]')).toHaveCount(1);
|
||||
await select(page,'0:uncertain.png');await expect(details(page).locator('[data-copy-role]')).toHaveCount(0);
|
||||
await expect(details(page)).toContainText('Not recorded in this file.');
|
||||
await expect(details(page).locator('p.prompt-text.muted').first()).toHaveText('Not recorded in this file.');
|
||||
});
|
||||
|
||||
test('copy toast and fallback dialog',async({page})=>{
|
||||
await page.addInitScript(()=>{window.copied=[];window.clipboardFails=false;Object.defineProperty(navigator,'clipboard',{configurable:true,value:{writeText:async text=>{if(window.clipboardFails)throw new Error('Permission denied');window.copied.push(text);}}});});
|
||||
await page.goto(workspace);await select(page,'0:shared/same.png');
|
||||
const positive=role(details(page),'Positive'),text=await positive.locator('p.prompt-text').textContent();
|
||||
const copy=positive.getByRole('button',{name:'Copy positive prompt',exact:true});await copy.click();
|
||||
await expect(page.locator('#workspace-toast')).toHaveText('Prompt copied.');expect(await page.evaluate(()=>window.copied)).toEqual([text]);
|
||||
await expect(positive.getByRole('status')).toHaveCount(0);
|
||||
await page.evaluate(()=>{window.clipboardFails=true;});await copy.click();
|
||||
const fallback=page.getByRole('dialog',{name:'Copy prompt',exact:true});await expect(fallback).toBeVisible();
|
||||
const area=fallback.getByRole('textbox',{name:'Select and copy this text',exact:true});
|
||||
await expect(area).toHaveValue(text);await expect(area).toBeFocused();
|
||||
expect(await area.evaluate(node=>[node.readOnly,node.selectionStart,node.selectionEnd])).toEqual([true,0,text.length]);
|
||||
await expect(fallback).toContainText('Automatic clipboard access requires a secure browser context.');
|
||||
await page.keyboard.press('Escape');await expect(fallback).toBeHidden();await expect(copy).toBeFocused();
|
||||
});
|
||||
|
||||
@@ -128,10 +128,10 @@ test('archive delete merge and prune preserve frozen evidence without repairing
|
||||
});
|
||||
|
||||
test('actual restore with reused numeric owner rejects stale history and offers explicit saved recovery',async({page,request})=>{
|
||||
const f=page.fixture;await open(page,f.detail_id);const original=await exact(saved(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('pre'));
|
||||
const f=page.fixture;await open(page,f.detail_id);const original=await exact(saved(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('p.prompt-text'));
|
||||
const source=await request.get('/__test__/history/reused-snapshot');await expect(source).toBeOK();await restoreSnapshot(request,await source.body());
|
||||
await marker(page,f.runs.shared).click();await expect(dialog(page)).toContainText('Recorded evidence changed');await page.keyboard.press('Escape');
|
||||
await expect(page.locator('.library-parent-status')).toContainText('changed');expect(await exact(saved(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('pre'))).toBe(original);
|
||||
await expect(page.locator('.library-parent-status')).toContainText('changed');expect(await exact(saved(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('p.prompt-text'))).toBe(original);
|
||||
await expect(recorded(page).getByRole('button',{name:'Reload recorded history',exact:true})).toBeVisible();
|
||||
await page.getByRole('button',{name:'Reload saved Library view',exact:true}).click();await expect(saved(page)).toContainText('Restored owner with reused numeric ID');
|
||||
expect((await request.get(`/prompt_manager/history/runs/${f.runs.shared}`)).status()).toBe(404);
|
||||
@@ -199,18 +199,20 @@ test('frozen exact roles distinguish unknown, explicit empty, whitespace, mode a
|
||||
const detail=await(await request.get(`/prompt_manager/history/runs/${f.runs.shared}?epoch=${window.history_epoch}`)).json();
|
||||
const chosen=detail.contexts.find(c=>c.anchor_id==='z');
|
||||
const pos=dialog(page).getByRole('region',{name:'Positive prompt',exact:true}),neg=dialog(page).getByRole('region',{name:'Negative prompt',exact:true});
|
||||
expect(await exact(pos.locator('pre'))).toBe(chosen.positive.raw);expect(await exact(neg.locator('pre'))).toBe(chosen.negative.raw);
|
||||
expect(await exact(pos.locator('p.prompt-text'))).toBe(chosen.positive.raw);expect(await exact(neg.locator('p.prompt-text'))).toBe(chosen.negative.raw);
|
||||
expect(await pos.evaluate(node=>node.nextElementSibling?.getAttribute('aria-label'))).toBe('Negative prompt');
|
||||
await dialog(page).getByRole('button',{name:'Effective',exact:true}).click();expect(await exact(neg.locator('pre'))).toBe(chosen.negative.effective);
|
||||
await dialog(page).getByRole('button',{name:'Effective',exact:true}).click();expect(await exact(neg.locator('p.prompt-text'))).toBe(chosen.negative.effective);
|
||||
await dialog(page).getByRole('button',{name:'Close recorded run',exact:true}).click();
|
||||
await marker(page,f.runs.unknown).click();await dialog(page).getByRole('button',{name:'Raw',exact:true}).click();
|
||||
const unknown=dialog(page).getByRole('region',{name:'Negative prompt',exact:true});await expect(unknown.getByRole('button')).toHaveCount(0);await expect(dialog(page).getByRole('button',{name:'Send prompt pair',exact:true})).toHaveCount(0);
|
||||
await dialog(page).getByRole('button',{name:'Effective',exact:true}).click();expect(await exact(unknown.locator('pre'))).toBe(' \n\t ');await expect(unknown).toContainText('host_cache_output');
|
||||
await dialog(page).getByRole('button',{name:'Effective',exact:true}).click();expect(await exact(unknown.locator('p.prompt-text'))).toBe(' \n\t ');
|
||||
// D22: the frozen block has no source line; each field's source stays in "Frozen field provenance".
|
||||
await expect(unknown).not.toContainText('host_cache_output');await expect(dialog(page).locator('.recorded-provenance')).toContainText(/Effective · \w+ · host_cache_output/);
|
||||
await dialog(page).getByRole('button',{name:'Close recorded run',exact:true}).click();
|
||||
await marker(page,f.runs.empty).click();await expect(dialog(page).getByRole('region',{name:'Negative prompt',exact:true})).toContainText('Empty negative prompt');
|
||||
await marker(page,f.runs.empty).click();await expect(dialog(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text')).toHaveText('No negative prompt (explicitly empty)');
|
||||
await expect(dialog(page).getByRole('button',{name:'Copy negative prompt',exact:true})).toBeVisible();await expect(dialog(page).getByRole('button',{name:'Send prompt pair',exact:true})).toBeVisible();
|
||||
await dialog(page).getByRole('button',{name:'Close recorded run',exact:true}).click();
|
||||
await marker(page,f.runs.whitespace).click();await dialog(page).getByRole('button',{name:'Raw',exact:true}).click();expect(await exact(dialog(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre'))).toBe(' \n\t ');
|
||||
await marker(page,f.runs.whitespace).click();await dialog(page).getByRole('button',{name:'Raw',exact:true}).click();expect(await exact(dialog(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text'))).toBe(' \n\t ');
|
||||
await expect(dialog(page)).toContainText('graph_literal');await expect(dialog(page)).toContainText('Role assignment: inferred');
|
||||
await dialog(page).getByText('Frozen field provenance',{exact:true}).click();await expect(dialog(page)).toContainText('Prepend');await expect(dialog(page)).toContainText('Append');await expect(dialog(page)).toContainText('graph_semantics');
|
||||
expect(shared.matching_anchors.map(a=>a.anchor_id)).toEqual(['10','2','z']);
|
||||
@@ -447,7 +449,7 @@ test('frozen Effective pair uses the shared acknowledged sender and Escape close
|
||||
});
|
||||
try {
|
||||
await open(page,page.fixture.detail_id);await marker(page,page.fixture.runs.unknown).click();await dialog(page).getByRole('button',{name:'Effective',exact:true}).click();
|
||||
const positive=await exact(dialog(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('pre'));
|
||||
const positive=await exact(dialog(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('p.prompt-text'));
|
||||
const send=dialog(page).getByRole('button',{name:'Send prompt pair',exact:true});await send.click();
|
||||
const sender=page.getByRole('dialog',{name:'Send to ComfyUI',exact:true});await sender.getByRole('combobox',{name:'ComfyUI window',exact:true}).selectOption({label:'History ComfyUI'});
|
||||
await sender.getByRole('combobox',{name:'Destination node',exact:true}).selectOption({label:'History destination'});await sender.getByRole('button',{name:'Preview changes',exact:true}).click();
|
||||
@@ -466,16 +468,16 @@ test('the inline current file names its prompts apart from the frozen run fields
|
||||
await open(page,page.fixture.detail_id);await marker(page,page.fixture.runs.shared).click();await expect(dialog(page).locator('[data-receipt]')).toHaveCount(5);
|
||||
await dialog(page).getByRole('button',{name:'Raw',exact:true}).click();
|
||||
const frozen=dialog(page).getByRole('region',{name:'Frozen run prompt texts',exact:true});
|
||||
const before=await exact(frozen.getByRole('region',{name:'Positive prompt',exact:true}).locator('pre'));
|
||||
const before=await exact(frozen.getByRole('region',{name:'Positive prompt',exact:true}).locator('p.prompt-text'));
|
||||
await dialog(page).locator('[data-receipt]').first().getByRole('button',{name:'View current file',exact:true}).click();
|
||||
await expect(page.locator('dialog#viewer-dialog')).toBeVisible();await page.keyboard.press('Escape');await expect(dialog(page)).toBeVisible();
|
||||
const current=dialog(page).getByRole('region',{name:'Current reported file',exact:true});await expect(current).toBeVisible();
|
||||
await expect(current.getByRole('region',{name:'Current file positive prompt',exact:true}).locator('pre')).toHaveText('current file positive');
|
||||
await expect(current.getByRole('region',{name:'Current file negative prompt',exact:true}).locator('pre')).toHaveText('current file negative');
|
||||
await expect(current.getByRole('region',{name:'Current file positive prompt',exact:true}).locator('p.prompt-text')).toHaveText('current file positive');
|
||||
await expect(current.getByRole('region',{name:'Current file negative prompt',exact:true}).locator('p.prompt-text')).toHaveText('current file negative');
|
||||
await expect(current.getByRole('button',{name:'Copy current file positive prompt',exact:true})).toBeVisible();
|
||||
for(const role of ['Positive','Negative'])await expect(dialog(page).getByRole('region',{name:`${role} prompt`,exact:true})).toHaveCount(1);
|
||||
await expect(current.getByRole('region',{name:/^(Positive|Negative) prompt$/})).toHaveCount(0);
|
||||
expect(await exact(dialog(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('pre'))).toBe(before);
|
||||
expect(await exact(dialog(page).getByRole('region',{name:'Positive prompt',exact:true}).locator('p.prompt-text'))).toBe(before);
|
||||
});
|
||||
|
||||
test('the recorded run dialog with a resolved current file has no duplicate button or region names',async({page})=>{
|
||||
@@ -574,7 +576,7 @@ test('history epoch recovery reports saved-parent read failures and retries with
|
||||
|
||||
test('changed saved-owner identity is disclosed without silently relabeling or rebinding the existing gallery',async({page,request})=>{
|
||||
const f=page.fixture;await open(page,f.detail_id);await showAllImages(page);await expect(gallery(page).locator('article[data-image-key]')).toHaveCount(1);
|
||||
const positive=saved(page).getByRole('region',{name:'Positive prompt',exact:true});const original=await exact(positive.locator('pre'));
|
||||
const positive=saved(page).getByRole('region',{name:'Positive prompt',exact:true});const original=await exact(positive.locator('p.prompt-text'));
|
||||
const cards=gallery(page).locator('article[data-image-key]');await cards.first().evaluate(node=>{window.savedGalleryCard=node;});let galleryReads=0;
|
||||
page.on('request',request=>{if(new URL(request.url()).pathname===`/prompt_manager/library/${f.detail_id}/images`)galleryReads++;});
|
||||
const details=await(await request.get(`/prompt_manager/library/${f.detail_id}`)).json();
|
||||
@@ -583,7 +585,7 @@ test('changed saved-owner identity is disclosed without silently relabeling or r
|
||||
await expect(await request.put(`/prompt_manager/prompts/${f.detail_id}`,{data:{text:'Replaced saved owner'}})).toBeOK();
|
||||
await page.route(/\/history\/runs\/[^/]+\/outputs\?/,async route=>route.fulfill({status:409,json:{success:false,history_version:1,code:'history-changed',error:'History changed',recovery:{action:'refresh-history'}}}));
|
||||
await marker(page,f.runs.shared).click();await expect(dialog(page)).toContainText('Recorded evidence changed');await dialog(page).getByRole('button',{name:'Close recorded run',exact:true}).click();
|
||||
await expect(page.locator('.library-parent-status')).toContainText('changed');expect(await exact(positive.locator('pre'))).toBe(original);
|
||||
await expect(page.locator('.library-parent-status')).toContainText('changed');expect(await exact(positive.locator('p.prompt-text'))).toBe(original);
|
||||
const reload=page.getByRole('button',{name:'Reload saved Library view',exact:true});await expect(reload).toBeVisible();
|
||||
expect(await cards.first().evaluate(node=>node===window.savedGalleryCard)).toBe(true);expect(galleryReads).toBe(0);
|
||||
await reload.click();await expect(saved(page).getByRole('region',{name:'Positive prompt',exact:true})).toContainText('Replaced saved owner');
|
||||
@@ -626,12 +628,12 @@ async function retainSavedSelection(page,request,fixture) {
|
||||
const index=details.variants.findIndex(variant=>variant.id===fixture.saved_pair);
|
||||
await page.getByRole('button',{name:`Select negative variant ${index+1}`,exact:true}).click();
|
||||
await expect(gallery(page).locator('article[data-image-key]')).toHaveCount(1);
|
||||
const negative=await exact(saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre'));
|
||||
const negative=await exact(saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text'));
|
||||
await gallery(page).locator('article[data-image-key]').first().evaluate(node=>{window.fix1SavedGalleryCard=node;});
|
||||
let reads=0;page.on('request',request=>{if(new URL(request.url()).pathname===`/prompt_manager/library/${fixture.detail_id}/images`)reads++;});
|
||||
return async()=>{
|
||||
await expect(page.getByRole('button',{name:`Select negative variant ${index+1}`,exact:true})).toHaveAttribute('aria-pressed','true');
|
||||
expect(await exact(saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre'))).toBe(negative);
|
||||
expect(await exact(saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text'))).toBe(negative);
|
||||
expect(await gallery(page).locator('article[data-image-key]').first().evaluate(node=>node===window.fix1SavedGalleryCard)).toBe(true);expect(reads).toBe(0);
|
||||
};
|
||||
}
|
||||
@@ -702,7 +704,7 @@ test('capture-only recovery uses the changed real output capture without re-read
|
||||
const outputs=await(await outputsRead).json();expect(outputs.capture.state).toBe('conflicting');expect(outputs.retained_occurrence_count).toBe(1);expect(outputs.list_status).toBe('incomplete');
|
||||
await expect(dialog(page).locator('[data-receipt]')).toHaveCount(1);await expect(dialog(page).locator('.recorded-output-status')).toContainText('1 retained reported output · incomplete · conflicting');
|
||||
await expect(dialog(page).getByText('Output capture',{exact:true}).locator('..').locator('dd')).toHaveText('conflicting · conflicting');expect(runReads).toBe(1);expect(outputsReads).toBe(2);
|
||||
expect(await exact(dialog(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre'))).toBe(' \n\t ');await page.keyboard.press('Escape');await retained();
|
||||
expect(await exact(dialog(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text'))).toBe(' \n\t ');await page.keyboard.press('Escape');await retained();
|
||||
});
|
||||
|
||||
test('completed page one stays bounded across mode changes until explicit output paging',async({page,request})=>{
|
||||
|
||||
@@ -531,7 +531,7 @@ test('add negative re-renders history with the saved or reused pair selected',as
|
||||
await expect(laneLabel(page,'Negative variant 1 ·')).toHaveCount(1);await expect(pressedVariant(page)).toHaveAccessibleName('Select negative variant 1');
|
||||
await add.click();dialog=editorDialog(page);await ready(dialog);await field(dialog,'Negative prompt').fill(` ${f.selected_negative.toUpperCase()} `);await button(dialog,'Save').click();
|
||||
await expect(dialog).toHaveCount(0);await expect(toast(page)).toHaveText('This prompt pair already exists. Showing the saved version.');
|
||||
await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre')).toHaveText(f.selected_negative);
|
||||
await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text')).toHaveText(f.selected_negative);
|
||||
await expect(laneLabel(page,/· 1 linked image$/)).toHaveCount(1);
|
||||
await expect(pressedVariant(page)).toHaveAccessibleName('Select negative variant 3');
|
||||
// The saved selection applies to that save only; returning opens on the first negative, as in the concept.
|
||||
@@ -550,7 +550,7 @@ test('reused pair outside first forty remains selected without loading every pag
|
||||
await openHistory(page,f.positive_id);await expect(variantsPanel(page)).toContainText('40 of 44 saved negative variants');page.writes.length=0;
|
||||
await variantsPanel(page).getByRole('button',{name:'+ Add negative',exact:true}).click();
|
||||
const dialog=editorDialog(page);await ready(dialog);await field(dialog,'Negative prompt').fill(f.first_negative);await button(dialog,'Save').click();await expect(dialog).toHaveCount(0);
|
||||
await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre')).toHaveText(f.first_negative);
|
||||
await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text')).toHaveText(f.first_negative);
|
||||
await expect(pressedVariant(page)).toHaveCount(1);await expect(variantsPanel(page)).toContainText('40 of 44 saved negative variants');
|
||||
await expect(variantsPanel(page).locator('.library-variant-lane')).toHaveCount(41);
|
||||
expect(variantCursorRequests).toHaveLength(0);expect(page.writes).toHaveLength(1);
|
||||
@@ -678,7 +678,7 @@ test('archive hides a pair, Show archived finds it and Restore keeps it selected
|
||||
const opacity=await lane.locator('.library-variant-dot').evaluate(node=>getComputedStyle(node).opacity);expect(Number(opacity)).toBeLessThan(1);
|
||||
const galleryReads=[];page.on('request',item=>{if(item.url().includes('/images?'))galleryReads.push(new URL(item.url()).searchParams);});
|
||||
await lane.locator('.library-variant-dot').click();await expect(lane.locator('.library-variant-dot')).toHaveAttribute('aria-pressed','true');
|
||||
await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre')).toHaveText(f.selected_negative);
|
||||
await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text')).toHaveText(f.selected_negative);
|
||||
await expect(page.getByRole('region',{name:'Shared gallery',exact:true}).locator('article[data-image-key]')).toHaveCount(1);
|
||||
// #249 D8 ruling (a): the Library-embedded gallery has no select checkbox and no selection bar.
|
||||
await expect(page.getByRole('region',{name:'Shared gallery',exact:true}).locator('.image-overlays input[type="checkbox"]')).toHaveCount(0);
|
||||
@@ -690,7 +690,7 @@ test('archive hides a pair, Show archived finds it and Restore keeps it selected
|
||||
await lane.getByRole('button',{name:/^Restore/}).click();
|
||||
await expect(variantsPanel(page).getByRole('button',{name:'Show archived (1)',exact:true})).toBeVisible();
|
||||
await expect(variantsPanel(page).locator('.library-variant-archived')).toHaveCount(1);
|
||||
await expect(pressedVariant(page)).toHaveCount(1);await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre')).toHaveText(f.selected_negative);
|
||||
await expect(pressedVariant(page)).toHaveCount(1);await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text')).toHaveText(f.selected_negative);
|
||||
expect(page.writes.slice(writes)).toEqual([{method:'PATCH',url:`/prompt_manager/pairs/${f.pair_id}`,body:{archived:false}}]);
|
||||
expect((await pairRead(request,f.pair_id)).archived).toBe(false);expect(await evidence()).toEqual(before);
|
||||
await expect(page.getByRole('region',{name:'Shared gallery',exact:true}).locator('article[data-image-key]')).toHaveCount(1);
|
||||
@@ -740,7 +740,7 @@ test('archived reuse is reported, shown under Show archived and selected read-on
|
||||
await expect(variantsPanel(page).getByRole('button',{name:'Show archived (1)',exact:true})).toHaveAttribute('aria-pressed','true');
|
||||
const lane=variantsPanel(page).locator('.library-variant-archived');await expect(lane).toHaveCount(1);
|
||||
await expect(lane.locator('.library-variant-dot')).toHaveAttribute('aria-pressed','true');
|
||||
await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre')).toHaveText(f.archived_negative);
|
||||
await expect(savedTexts(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text')).toHaveText(f.archived_negative);
|
||||
await expect.poll(()=>galleryReads.at(-1)?.get('archive')).toBe('archived');expect(galleryReads.at(-1).get('pair_id')).toBe(String(f.archived_pair_id));
|
||||
await uniqueDotNames(page);
|
||||
await page.goto(`${workspace}#/library`);await page.getByRole('button',{name:'+ New prompt',exact:true}).click();dialog=editorDialog(page);await ready(dialog);
|
||||
|
||||
@@ -45,7 +45,7 @@ test('history displays the selected full negative beneath its positive and filte
|
||||
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(neg.locator('p.prompt-text')).toHaveText(negative||'No negative prompt (explicitly empty)');
|
||||
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');
|
||||
@@ -187,7 +187,7 @@ test('history shares real image metadata, vendored Viewer and the acknowledged p
|
||||
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();
|
||||
const chosenNegative=await saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text').textContent();
|
||||
await page.locator('#workspace-main > .heading').getByRole('button',{name:'Send prompt pair to ComfyUI: Cat portrait',exact:true}).click();
|
||||
const sender=page.getByRole('dialog',{name:'Send to ComfyUI',exact:true});
|
||||
const hosts=sender.getByRole('combobox',{name:'ComfyUI window',exact:true});
|
||||
@@ -211,10 +211,10 @@ test('large variant histories append bounded pages without replacing the selecte
|
||||
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 selectVariant(page,0);const chosenNegative=await saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text').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);
|
||||
await expect(saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text')).toHaveText(chosenNegative);
|
||||
});
|
||||
|
||||
async function observeLibrary(page, hold=false) {
|
||||
@@ -283,7 +283,7 @@ test('Library natural rows retain long prompt content without overflow',async({p
|
||||
await expect(page.locator('#workspace-main p').filter({hasText:'One positive. Reusable negatives.'})).toHaveCount(1);
|
||||
for(const theme of ['dark','light']) {
|
||||
await page.getByRole('combobox',{name:'Theme',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('.prompt-section.positive .role-label').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');
|
||||
@@ -443,5 +443,5 @@ test('saved choices retain curved staggered lanes and the full negative immediat
|
||||
const rows=await(await request.get('/prompt_manager/library')).json();const cat=rows.items.find(item=>item.text==='Cat portrait');
|
||||
const details=await(await request.get(`/prompt_manager/library/${cat.id}`)).json();
|
||||
const selected=details.variants[1].negative_text;
|
||||
expect(await saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('pre').evaluate(node=>node.textContent)).toBe(selected);
|
||||
expect(await saved(page).getByRole('region',{name:'Negative prompt',exact:true}).locator('p.prompt-text').evaluate(node=>node.textContent)).toBe(selected);
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ async function attachEvidence(page, testInfo, name, fullPage = true) {
|
||||
const screenshotPath = testInfo.outputPath(`${name}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage });
|
||||
await testInfo.attach(name, { path: screenshotPath, contentType: 'image/png' });
|
||||
const geometry = await page.evaluate(() => Object.fromEntries(['.sidebar', '.topbar', '#workspace-main', '.heading', 'h1', '.image-workspace', '.image-grid', '.media-card', '.image-details', '.prompt-block-heading', '.library-toolbar', '.library-grid', '.library-card', '.library-card-art', '.library-card-content'].map(selector => {
|
||||
const geometry = await page.evaluate(() => Object.fromEntries(['.sidebar', '.topbar', '#workspace-main', '.heading', 'h1', '.image-workspace', '.image-grid', '.media-card', '.image-details', '.prompt-heading', '.library-toolbar', '.library-grid', '.library-card', '.library-card-art', '.library-card-content'].map(selector => {
|
||||
const node = document.querySelector(selector);
|
||||
if (!node) return [selector, null];
|
||||
const box = node.getBoundingClientRect(), style = getComputedStyle(node);
|
||||
@@ -101,7 +101,7 @@ for (const [width, height] of [[1600, 1100], [390, 844]]) for (const theme of ['
|
||||
const roles = page.getByRole('region', { name: 'Saved prompt texts', exact: true });
|
||||
const positive = roles.getByRole('region', { name: 'Positive prompt', exact: true });
|
||||
expect(await positive.evaluate(node => node.nextElementSibling?.getAttribute('aria-label'))).toBe('Negative prompt');
|
||||
for (const name of ['Copy positive prompt', 'Send positive prompt', 'Copy negative prompt', 'Send negative prompt']) await expect(roles.getByRole('button', { name, exact: true })).toBeVisible();
|
||||
for (const name of ['Copy positive prompt', 'Send positive prompt to ComfyUI', 'Copy negative prompt', 'Send negative prompt to ComfyUI']) await expect(roles.getByRole('button', { name, exact: true })).toBeVisible();
|
||||
// As in the concept, the selected pair's send sits in the history heading, not among the saved texts.
|
||||
await expect(roles.getByRole('button', { name: 'Send prompt pair', exact: true })).toHaveCount(0);
|
||||
await expect(page.locator('#workspace-main > .heading button.send')).toBeEnabled();
|
||||
@@ -125,11 +125,14 @@ for (const [width, height] of [[1600, 1100], [390, 844]]) for (const theme of ['
|
||||
const viewer = page.locator('dialog#viewer-dialog');await expect(viewer).toBeVisible();
|
||||
await expect(viewer.getByRole('region', { name: 'Positive prompt', exact: true })).toContainText(fixture.canonical_positive);
|
||||
await expect(viewer.getByRole('region', { name: 'Negative prompt', exact: true })).toContainText(fixture.canonical_negative);
|
||||
for (const name of ['Copy positive prompt', 'Send positive prompt', 'Copy negative prompt', 'Send negative prompt', 'Send prompt pair']) await expect(viewer.getByRole('button', { name, exact: true })).toBeVisible();
|
||||
for (const name of ['Copy positive prompt', 'Send positive prompt to ComfyUI', 'Copy negative prompt', 'Send negative prompt to ComfyUI', 'Send prompt pair']) await expect(viewer.getByRole('button', { name, exact: true })).toBeVisible();
|
||||
await expect(viewer).toContainText('42001');await expect(viewer).toContainText('720');await expect(viewer).toContainText('480');
|
||||
const currentPositive = viewer.getByRole('region', { name: 'Positive prompt', exact: true });
|
||||
await currentPositive.getByRole('button', { name: 'Copy positive prompt', exact: true }).click();
|
||||
await expect(currentPositive.getByRole('status')).toContainText(/Copied\.|Copy unavailable/);
|
||||
// The concept's copy outcome: the shared toast, or its "Copy prompt" dialog when the clipboard is refused.
|
||||
const copyFallback = page.getByRole('dialog', { name: 'Copy prompt', exact: true });
|
||||
await expect(page.locator('#workspace-toast').filter({ hasText: 'Prompt copied.' }).or(copyFallback)).toHaveCount(1);
|
||||
if (await copyFallback.isVisible()) { await page.keyboard.press('Escape'); await expect(copyFallback).toBeHidden(); }
|
||||
await exerciseCurrentPairSend(page, { positive: fixture.canonical_positive, negative: fixture.canonical_negative });
|
||||
await captureCanonicalState(page, testInfo, `production-viewer-${width}-${theme}`, 'dialog#viewer-dialog');
|
||||
await page.keyboard.press('Escape');await expect(view).toBeFocused();await page.keyboard.press('Escape');await expect(run).toBeFocused();
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as module from '../../web/js/workspace/prompt-inspector.mjs';
|
||||
|
||||
function dom() {
|
||||
class Element {
|
||||
constructor(tag){this.tagName=tag;this.children=[];this.dataset={};this.attributes={};this.listeners=new Map();this.classList={add:()=>{}};}
|
||||
constructor(tag){this.tagName=tag;this.children=[];this.dataset={};this.attributes={};this.listeners=new Map();this.classList={add:()=>{}};this.className='';}
|
||||
append(...children){for(const child of children){child.parent=this;this.children.push(child);}}
|
||||
replaceChildren(...children){this.children=[];this.append(...children);}
|
||||
setAttribute(key,value){this.attributes[key]=value;}
|
||||
@@ -13,9 +13,9 @@ function dom() {
|
||||
closest(selector){if(selector==='section'&&this.tagName==='section')return this;if(selector==='[data-send-role]'&&Object.hasOwn(this.dataset,'sendRole'))return this;if(selector==='[data-copy-role]'&&Object.hasOwn(this.dataset,'copyRole'))return this;if(selector==='[data-retry-prompts]'&&Object.hasOwn(this.dataset,'retryPrompts'))return this;return this.parent?.closest(selector);}
|
||||
click(target){this.listeners.get('click')?.({target});}
|
||||
}
|
||||
const previous=global.document;global.document={createElement:tag=>new Element(tag),createElementNS:(_,tag)=>new Element(tag)};
|
||||
const previous=global.document,toast=new Element('div');global.document={createElement:tag=>new Element(tag),createElementNS:(_,tag)=>new Element(tag),getElementById:id=>id==='workspace-toast'?toast:null};
|
||||
const walk=(root,predicate)=>[...(predicate(root)?[root]:[]),...root.children.flatMap(child=>walk(child,predicate))];
|
||||
return {Element,walk,restore:()=>{global.document=previous;}};
|
||||
return {Element,walk,toast,restore:()=>{global.document=previous;}};
|
||||
}
|
||||
const record=text=>({status:'resolved',text,source:'library'});
|
||||
test('shared prompt blocks send complete selected library values and explicit empty negatives',()=>{
|
||||
@@ -27,7 +27,7 @@ test('shared prompt blocks send complete selected library values and explicit em
|
||||
view.update({positive:record('Complete positive'),negative:record('')});
|
||||
const pair=f.walk(container,node=>node.dataset.sendRole==='pair')[0];container.click(pair);
|
||||
assert.deepEqual(sent,[{positive:'Complete positive',negative:''}]);
|
||||
assert.equal(f.walk(container,node=>node.textContent==='Saved library prompt').length,2);
|
||||
assert.equal(f.walk(container,node=>node.textContent==='Saved library prompt').length,0,'no per-block source line');
|
||||
view.dispose();container.click(pair);assert.equal(sent.length,1);
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
@@ -57,13 +57,13 @@ test('frozen fields disclose their exact source and keep null, empty and whitesp
|
||||
try{
|
||||
const view=module.mountPromptBlocks(container,{parentScope:scope,sender:{open:roles=>sent.push(roles)},prompts:{positive:field('positive','runtime_return'),negative:field(null,'unknown')}});
|
||||
assert.equal(f.walk(container,node=>node.dataset.sendRole==='negative'||node.dataset.sendRole==='pair'||node.dataset.copyRole==='negative').length,0);
|
||||
assert.ok(f.walk(container,node=>node.textContent?.includes('runtime_return')).length);
|
||||
assert.equal(f.walk(container,node=>node.textContent?.includes('runtime_return')).length,0,'no per-block source line');
|
||||
view.update({positive:field('positive','runtime_return'),negative:field('','host_cache_output')});
|
||||
assert.ok(f.walk(container,node=>node.textContent?.includes('host_cache_output')).length);
|
||||
assert.equal(f.walk(container,node=>node.textContent?.includes('host_cache_output')).length,0);
|
||||
const pair=f.walk(container,node=>node.dataset.sendRole==='pair')[0];container.click(pair);
|
||||
assert.deepEqual(sent,[{positive:'positive',negative:''}]);
|
||||
view.update({positive:field('positive','runtime_return'),negative:field(' \n\t ','host_cache_output')});
|
||||
assert.equal(f.walk(container,node=>node.tagName==='pre'&&node.textContent===' \n\t ').length,1);
|
||||
assert.equal(f.walk(container,node=>node.tagName==='p'&&node.className==='prompt-text'&&node.textContent===' \n\t ').length,1);
|
||||
container.click(f.walk(container,node=>node.dataset.sendRole==='negative')[0]);
|
||||
assert.deepEqual(sent[1],{negative:' \n\t '});
|
||||
view.setState({prompts:{positive:field('positive','runtime_return'),negative:field('old','host_cache_output')},actionsEnabled:false});
|
||||
@@ -71,19 +71,19 @@ test('frozen fields disclose their exact source and keep null, empty and whitesp
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
|
||||
test('a zeroed conditioning negative shows as an explicit empty negative with its own source',()=>{
|
||||
test('a zeroed conditioning negative shows as an explicit empty negative without a source line',()=>{
|
||||
const f=dom(),scope=createScope(),container=new f.Element('div'),sent=[];
|
||||
try{
|
||||
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:roles=>sent.push(roles)},prompts:{positive:{status:'resolved',text:'positive',source:'graph-inferred'},negative:{status:'resolved',text:'',source:'zeroed-conditioning'}}});
|
||||
assert.equal(f.walk(container,node=>node.textContent==='Zeroed conditioning (no negative text)').length,1);
|
||||
assert.equal(f.walk(container,node=>node.textContent==='Empty negative prompt').length,1);
|
||||
assert.equal(f.walk(container,node=>node.textContent==='Zeroed conditioning (no negative text)').length,0);
|
||||
assert.equal(f.walk(container,node=>node.className==='prompt-text'&&node.textContent==='No negative prompt (explicitly empty)').length,1);
|
||||
container.click(f.walk(container,node=>node.dataset.sendRole==='pair')[0]);
|
||||
assert.deepEqual(sent,[{positive:'positive',negative:''}]);
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
|
||||
const unresolved=message=>({status:'unresolved',text:null,source:null,...(message===undefined?{}:{message})});
|
||||
const statusLines=(f,container)=>f.walk(container,node=>node.tagName==='p'&&node.className==='muted').map(node=>node.textContent);
|
||||
const statusLines=(f,container)=>f.walk(container,node=>node.tagName==='p'&&node.className==='prompt-text muted').map(node=>node.textContent);
|
||||
test('unsupported node status names the class',()=>{
|
||||
const f=dom(),scope=createScope(),container=new f.Element('div');
|
||||
const line='Prompt uses a node the inspector does not support (DualCFGGuider).';
|
||||
@@ -101,7 +101,7 @@ test('runtime and composite statuses',()=>{
|
||||
assert.deepEqual(statusLines(f,container),[runtime,runtime]);
|
||||
view.update({positive:unresolved(composite),negative:{status:'resolved',text:'kept',source:'execution'}});
|
||||
assert.deepEqual(statusLines(f,container),[composite]);
|
||||
assert.equal(f.walk(container,node=>node.tagName==='pre'&&node.textContent==='kept').length,1);
|
||||
assert.equal(f.walk(container,node=>node.tagName==='p'&&node.className==='prompt-text'&&node.textContent==='kept').length,1);
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
test('limit status',()=>{
|
||||
@@ -136,10 +136,59 @@ test('a context label names nested prompt regions and actions apart from the fro
|
||||
try{
|
||||
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},contextLabel:'Current file',prompts:{positive:record('p'),negative:record('n')}});
|
||||
const labels=f.walk(container,node=>node.attributes['aria-label']).map(node=>node.attributes['aria-label']);
|
||||
for(const name of ['Current file positive prompt','Current file negative prompt','Copy current file positive prompt','Send current file positive prompt','Copy current file negative prompt','Send current file negative prompt']) assert.ok(labels.includes(name),name);
|
||||
for(const name of ['Current file positive prompt','Current file negative prompt','Copy current file positive prompt','Send current file positive prompt to ComfyUI','Copy current file negative prompt','Send current file negative prompt to ComfyUI']) assert.ok(labels.includes(name),name);
|
||||
assert.equal(labels.filter(name=>/^(Positive|Negative) prompt$/.test(name)).length,0);
|
||||
assert.deepEqual(f.walk(container,node=>node.tagName==='h3').map(node=>node.textContent),['Positive prompt','Negative prompt']);
|
||||
assert.deepEqual(f.walk(container,node=>node.className==='role-label').map(node=>node.textContent),['+ Positive prompt','\u2212 Negative prompt']);
|
||||
const plain=new f.Element('div');module.mountPromptBlocks(plain,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:record('p')}});
|
||||
assert.ok(f.walk(plain,node=>node.attributes['aria-label']==='Positive prompt').length);
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
|
||||
const shape=node=>({tag:node.tagName,className:node.className,...(node.attributes['aria-label']?{label:node.attributes['aria-label']}:{}),
|
||||
...(node.children.length?{children:node.children.filter(child=>child.tagName!=='svg').map(shape)}:{text:node.textContent})});
|
||||
test('prompt block emits concept markup without a source line',()=>{
|
||||
const f=dom(),scope=createScope(),container=new f.Element('div');
|
||||
try{
|
||||
module.mountPromptBlocks(container,{parentScope:scope,includePairAction:false,sender:{open:()=>{}},prompts:{positive:{...record('a lighthouse'),evidence:{provenance:'recorded',source:'runtime_return'}},negative:record('blurry')}});
|
||||
const section=(role,label,text)=>({tag:'section',className:`prompt-section ${role}`,label:`${role[0].toUpperCase()}${role.slice(1)} prompt`,children:[
|
||||
{tag:'div',className:'prompt-heading',children:[{tag:'span',className:'role-label',text:label},{tag:'div',className:'prompt-actions',children:[
|
||||
{tag:'button',className:'icon-btn',label:`Copy ${role} prompt`,children:[]},{tag:'button',className:'send icon-btn',label:`Send ${role} prompt to ComfyUI`,children:[]}]}]},
|
||||
{tag:'p',className:'prompt-text',text}]});
|
||||
assert.deepEqual(container.children.map(shape),[section('positive','+ Positive prompt','a lighthouse'),section('negative','\u2212 Negative prompt','blurry')]);
|
||||
for(const button of f.walk(container,node=>node.tagName==='button')) assert.equal(button.children.filter(child=>child.tagName==='svg').length,1,'one icon per action');
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
test('empty negative uses the concept copy and still sends an explicit clear',()=>{
|
||||
const f=dom(),scope=createScope(),container=new f.Element('div'),sent=[];
|
||||
try{
|
||||
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:roles=>sent.push(roles)},prompts:{positive:record('positive'),negative:record('')}});
|
||||
const negative=container.children[1];
|
||||
assert.equal(negative.children[1].className,'prompt-text');assert.equal(negative.children[1].textContent,'No negative prompt (explicitly empty)');
|
||||
container.click(f.walk(negative,node=>node.dataset.sendRole==='negative')[0]);
|
||||
container.click(f.walk(container,node=>node.dataset.sendRole==='pair')[0]);
|
||||
assert.deepEqual(sent,[{negative:''},{positive:'positive',negative:''}]);
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
test('unknown roles never offer copy or send',()=>{
|
||||
const f=dom(),scope=createScope(),container=new f.Element('div'),sent=[];
|
||||
try{
|
||||
module.mountPromptBlocks(container,{parentScope:scope,roles:['positive','negative','style'],sender:{open:roles=>sent.push(roles)},
|
||||
prompts:{positive:{status:'absent'},negative:{status:'ambiguous',text:'candidate'},style:record('not a role')}});
|
||||
assert.deepEqual(container.children.map(node=>node.className),['prompt-section positive','prompt-section negative'],'an unknown role name renders nothing');
|
||||
assert.deepEqual(statusLines(f,container),['Not recorded in this file.','Multiple possible prompts. Source could not be determined.']);
|
||||
assert.equal(f.walk(container,node=>node.dataset.sendRole!==undefined||node.dataset.copyRole!==undefined||node.className==='prompt-actions').length,0);
|
||||
const forged=new f.Element('button');forged.dataset.sendRole='style';container.append(forged);container.click(forged);
|
||||
assert.equal(sent.length,0,'a forged action for an unknown role sends nothing');
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
test('copy shows the shared toast "Prompt copied." with the exact role text',async()=>{
|
||||
const f=dom(),scope=createScope(),container=new f.Element('div'),copied=[];
|
||||
const previous=Object.getOwnPropertyDescriptor(globalThis,'navigator');
|
||||
Object.defineProperty(globalThis,'navigator',{configurable:true,value:{clipboard:{writeText:async text=>{copied.push(text);}}}});
|
||||
try{
|
||||
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:()=>{}},prompts:{positive:record(' exact\ntext '),negative:record('')}});
|
||||
await container.listeners.get('click')({target:f.walk(container,node=>node.dataset.copyRole==='positive')[0]});
|
||||
assert.deepEqual(copied,[' exact\ntext ']);assert.equal(f.toast.textContent,'Prompt copied.');
|
||||
assert.equal(f.walk(container,node=>node.attributes.role==='status').length,0,'no per-block copy status line');
|
||||
}finally{scope.dispose();f.restore();if(previous)Object.defineProperty(globalThis,'navigator',previous);else delete globalThis.navigator;}
|
||||
});
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
.options-status { font-size: 12px; padding-inline: 4px; }
|
||||
|
||||
.prompt-inspector { margin-block-start: 22px; min-inline-size: 0; }
|
||||
/* The Library editor's Archive negative dialog keeps its own `section.prompt-block` (h3, `pre`, "Empty negative
|
||||
prompt") and the recorded run's "Frozen field provenance" keeps `pre.prompt-text` until their page rebuilds;
|
||||
shared prompt blocks are the concept's `.prompt-section` with `p.prompt-text` (#249 D22). */
|
||||
.prompt-block { border-block-start: 1px solid var(--ca-line); padding-block: 13px; min-inline-size: 0; }
|
||||
.prompt-positive { --prompt-role: #73dda9; }
|
||||
[data-ca-theme="light"] .prompt-positive { --prompt-role: #176b46; }
|
||||
@@ -23,18 +26,8 @@
|
||||
[data-ca-theme="light"] .prompt-negative { --prompt-role: #b71931; }
|
||||
.prompt-block-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
|
||||
.prompt-block h3 { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; margin: 0; color: var(--prompt-role); }
|
||||
.prompt-source, .prompt-empty, .prompt-block > .muted { font-size: 10px; }
|
||||
.prompt-source { margin-block: 6px 10px; }
|
||||
/* #249 D22 interim: production's prompt blocks keep their text, action row and 26px buttons until PR D renders the
|
||||
concept prompt block and retires these three overrides. */
|
||||
.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; white-space: pre-wrap; overflow-wrap: anywhere; margin-block: 10px; }
|
||||
.prompt-text:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 3px; }
|
||||
.prompt-actions { /* #249 D22 interim (PR D) */ display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.ca-tools .prompt-actions button { /* #249 D22 interim (PR D) */ display: inline-flex; align-items: center; justify-content: center; inline-size: 26px; block-size: 26px; min-block-size: 26px; padding: 5px; }
|
||||
.prompt-actions .prompt-send-button span { display: none; }
|
||||
.prompt-actions .icon { inline-size: 14px; block-size: 14px; }
|
||||
.prompt-copy-status { font-size: 11px; margin-block-end: 0; }
|
||||
.prompt-copy-status:empty { display: none; }
|
||||
.prompt-empty, .prompt-block > .muted { font-size: 10px; }
|
||||
pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; white-space: pre-wrap; overflow-wrap: anywhere; margin-block: 10px; }
|
||||
.prompt-send-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; color: var(--ca-focus); }
|
||||
.prompt-send-button svg { inline-size: 16px; block-size: 16px; flex-shrink: 0; }
|
||||
.prompt-sender { background: var(--ca-surface); color: var(--ca-text); border: 1px solid var(--ca-line); border-radius: 12px; padding: 22px; inline-size: min(760px, calc(100% - 32px)); max-inline-size: none; max-block-size: calc(100dvh - 32px); overflow: auto; }
|
||||
|
||||
@@ -13,9 +13,7 @@
|
||||
.file-evidence { color: var(--ca-focus); }
|
||||
.file-format { flex-shrink: 0; max-inline-size: 50%; }
|
||||
.file-inspector .prompt-inspector { margin-block-start: 15px; }
|
||||
.file-inspector .prompt-block { padding-block: 15px 0; margin-block-start: 15px; }
|
||||
.file-inspector .prompt-text { margin-block: 8px 0; }
|
||||
.file-inspector .prompt-negative { margin-block-end: 12px; }
|
||||
.file-inspector .prompt-section.negative { margin-block-end: 12px; }
|
||||
.file-vocabulary > p { margin-block: 0 6px; font-size: 10px; }
|
||||
.file-vocabulary .file-chip { font-size: 12px; }
|
||||
.generation-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-block: 16px; font-size: 11px; }
|
||||
|
||||
@@ -19,7 +19,7 @@ main.library-main { --library-positive: #73dda9; }
|
||||
.library-main .library-card-title h2 { margin: 0; font-size: 16px; font-weight: 600; min-inline-size: 0; overflow-wrap: anywhere; }
|
||||
.library-main .library-title-link { color: var(--ca-text); text-decoration: none; }
|
||||
.library-main .library-title-link:hover { color: var(--ca-accent); }
|
||||
.library-main .library-card-prompts .prompt-block { margin: 0; padding-block: 10px 0; }
|
||||
.library-main .library-card-prompts .prompt-section { margin: 0; padding-block: 10px 0; }
|
||||
.library-main .library-card-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 9px; font-size: 10px; color: var(--ca-muted); }
|
||||
.library-main .library-chip { display: inline-block; max-inline-size: 100%; padding: 3px 8px; border: 1px solid var(--ca-line); border-radius: 4px; font-size: 10px; color: var(--ca-muted); background: var(--ca-field); overflow-wrap: anywhere; }
|
||||
.library-main .library-chip-negative { color: var(--ca-error-text); border-color: color-mix(in srgb, var(--ca-error) 35%, var(--ca-line)); }
|
||||
@@ -95,7 +95,7 @@ main.library-main { --library-negative: #ff8c98; }
|
||||
.recorded-run-facts dd { margin: 4px 0 0; white-space: pre-wrap; }
|
||||
.recorded-run-anchors, .recorded-run-mode { display: flex; flex-wrap: wrap; gap: 8px; margin-block: 18px; }
|
||||
.recorded-run-dialog [aria-pressed="true"] { outline: 2px solid var(--ca-focus); outline-offset: 2px; }
|
||||
.recorded-run-dialog :where(.muted, .prompt-source, .recorded-output-status) { font-size: 11px; line-height: 1.65; }
|
||||
.recorded-run-dialog :where(.muted, .recorded-output-status) { font-size: 11px; line-height: 1.65; }
|
||||
.recorded-provenance { border-block-start: 1px solid var(--ca-line); padding-block-start: 12px; margin-block: 18px; }
|
||||
.recorded-provenance summary { cursor: pointer; }
|
||||
.recorded-run-dialog pre { white-space: pre-wrap; overflow-wrap: anywhere; max-inline-size: 100%; }
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
import { createScope, createButton } from '../shared/toolkit/core.js';
|
||||
import { createSendButton } from './prompt-sender.mjs';
|
||||
import { createWorkspaceIcon } from './icons.mjs';
|
||||
import { showWorkspaceToast } from './toast.mjs';
|
||||
import { openCopyFallback } from './workspace-dialogs.mjs';
|
||||
|
||||
export function promptRoles(prompts) {
|
||||
return Object.fromEntries(['positive', 'negative'].filter(role => prompts?.[role]?.status === 'resolved' && typeof prompts[role].text === 'string').map(role => [role, prompts[role].text]));
|
||||
}
|
||||
|
||||
const sources = {
|
||||
library: 'Saved library prompt',
|
||||
execution: 'Recorded at execution',
|
||||
'graph-inferred': 'Inferred from the saved workflow',
|
||||
'embedded-parameters': 'Embedded generation parameters',
|
||||
'zeroed-conditioning': 'Zeroed conditioning (no negative text)',
|
||||
'saved-association': 'Saved library association',
|
||||
};
|
||||
// #249 D22: the concept's prompt block (app.js a298–302) carries no source line; a file's evidence is its panel badge
|
||||
// (D21) and its "File & provenance" line. Roles without a confirmed text keep these status strings.
|
||||
const statuses = {
|
||||
absent: 'Not recorded in this file.',
|
||||
ambiguous: 'Multiple possible prompts. Source could not be determined.',
|
||||
unresolved: 'Prompt could not be resolved.',
|
||||
};
|
||||
const fieldSources = {
|
||||
runtime_return: 'Runtime return',
|
||||
host_cache_output: 'Host cache output',
|
||||
graph_literal: 'Inferred graph literal',
|
||||
graph_semantics: 'Inferred graph semantics',
|
||||
};
|
||||
const ROLE_LABELS = Object.freeze({positive: '+ Positive prompt', negative: '\u2212 Negative prompt'});
|
||||
function el(tag, text, className) {
|
||||
const node = document.createElement(tag);
|
||||
if (text !== undefined) node.textContent = text;
|
||||
@@ -44,7 +35,7 @@ function childScope(parentScope) {
|
||||
export function mountPromptBlocks(container, {prompts, sender, parentScope, roles:visibleRoles = ['positive', 'negative'], onRetry, includePairAction = true, contextLabel = ''}) {
|
||||
// A nested set of blocks (the current file inside a recorded run) names its regions and actions with a
|
||||
// context so assistive technology never meets two identically named prompt regions in one dialog.
|
||||
const named = role => contextLabel ? `${contextLabel} ${role} prompt` : `${role} prompt`;
|
||||
const named = title => contextLabel ? `${contextLabel} ${title.toLowerCase()} prompt` : `${title} prompt`;
|
||||
const action = role => contextLabel ? `${contextLabel[0].toLowerCase()}${contextLabel.slice(1)} ${role} prompt` : `${role} prompt`;
|
||||
if (parentScope.signal.aborted) return {update() {}, setState() {}, dispose() {}};
|
||||
const scope = childScope(parentScope);
|
||||
@@ -52,19 +43,20 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role
|
||||
scope.listen(container, 'click', async event => {
|
||||
if (!actionsEnabled) return;
|
||||
const send = event.target.closest('[data-send-role]');
|
||||
if (send) {
|
||||
if (send && (send.dataset.sendRole === 'pair' || Object.hasOwn(roles, send.dataset.sendRole))) {
|
||||
const role = send.dataset.sendRole;
|
||||
sender.open(role === 'pair' ? roles : {[role]:roles[role]}, send);
|
||||
}
|
||||
const copy = event.target.closest('[data-copy-role]');
|
||||
if (copy) {
|
||||
const role = copy.dataset.copyRole;
|
||||
const status = copy.closest('section').querySelector('.prompt-copy-status');
|
||||
if (copy && Object.hasOwn(roles, copy.dataset.copyRole)) {
|
||||
// The concept's copy: the shared toast, or its "Copy prompt" select-to-copy dialog without a clipboard.
|
||||
const text = roles[copy.dataset.copyRole];
|
||||
try {
|
||||
await navigator.clipboard.writeText(roles[role]);
|
||||
if (!scope.signal.aborted) status.textContent = 'Copied.';
|
||||
if (!navigator.clipboard) throw new Error('Clipboard requires a secure context');
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (!scope.signal.aborted) showWorkspaceToast('Prompt copied.');
|
||||
} catch {
|
||||
if (!scope.signal.aborted) status.textContent = 'Copy unavailable. Select the prompt text to copy it.';
|
||||
if (!scope.signal.aborted) openCopyFallback(text, {scope, trigger:copy});
|
||||
}
|
||||
}
|
||||
if (event.target.closest('[data-retry-prompts]')) onRetry?.();
|
||||
@@ -87,38 +79,32 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role
|
||||
return;
|
||||
}
|
||||
roles = Object.fromEntries(Object.entries(promptRoles(state.prompts)).filter(([role]) => visibleRoles.includes(role)));
|
||||
for (const role of visibleRoles) {
|
||||
const title = `${role[0].toUpperCase() + role.slice(1)} prompt`;
|
||||
for (const role of visibleRoles.filter(name => Object.hasOwn(ROLE_LABELS, name))) {
|
||||
const record = state.prompts?.[role];
|
||||
const block = el('section', undefined, `prompt-block prompt-${role}`);
|
||||
block.setAttribute('aria-label', contextLabel ? named(role) : title);
|
||||
const heading = el('div', undefined, 'prompt-block-heading');
|
||||
heading.append(el('h3', title));
|
||||
const block = el('section', undefined, `prompt-section ${role}`);
|
||||
block.setAttribute('aria-label', named(role[0].toUpperCase() + role.slice(1)));
|
||||
const heading = el('div', undefined, 'prompt-heading');
|
||||
heading.append(el('span', ROLE_LABELS[role], 'role-label'));
|
||||
block.append(heading);
|
||||
if (Object.hasOwn(roles, role)) {
|
||||
const proof = record.evidence;
|
||||
block.append(el('p', proof ? `${fieldSources[proof.source] || 'Source unknown'} · ${proof.source}` : sources[record.source] || 'Source not recorded', 'prompt-source muted'));
|
||||
if (roles[role] === '') block.append(el('p', `Empty ${role} prompt`, 'prompt-empty'));
|
||||
else {
|
||||
const text = el('pre', roles[role], 'prompt-text');
|
||||
text.tabIndex = 0;
|
||||
block.append(text);
|
||||
}
|
||||
const actions = el('div', undefined, 'prompt-actions');
|
||||
const send = createSendButton(`Send ${action(role)}`);
|
||||
send.dataset.sendRole = role;
|
||||
send.disabled = !actionsEnabled;
|
||||
const copy = createButton('', 'ca-icon-button');
|
||||
const copy = createButton('', 'icon-btn');
|
||||
copy.setAttribute('aria-label', `Copy ${action(role)}`);
|
||||
copy.title = `Copy ${action(role)}`;
|
||||
copy.append(createWorkspaceIcon('copy'));
|
||||
copy.dataset.copyRole = role;
|
||||
copy.disabled = !actionsEnabled;
|
||||
const send = createButton('', 'send icon-btn');
|
||||
send.setAttribute('aria-label', `Send ${action(role)} to ComfyUI`);
|
||||
send.append(createWorkspaceIcon('send'));
|
||||
send.dataset.sendRole = role;
|
||||
send.disabled = !actionsEnabled;
|
||||
actions.append(copy, send);
|
||||
heading.append(actions);
|
||||
const status = el('p', '', 'prompt-copy-status muted'); status.setAttribute('role', 'status');
|
||||
block.append(status);
|
||||
} else block.append(el('p', typeof record?.message === 'string' && record.message ? record.message : statuses[record?.status] || statuses.unresolved, 'muted'));
|
||||
const text = el('p', roles[role] === '' ? `No ${role} prompt (explicitly empty)` : roles[role], 'prompt-text');
|
||||
// Production keeps a confirmed prompt keyboard-reachable for reading and selecting long text.
|
||||
text.tabIndex = 0;
|
||||
block.append(text);
|
||||
} else block.append(el('p', typeof record?.message === 'string' && record.message ? record.message : statuses[record?.status] || statuses.unresolved, 'prompt-text muted'));
|
||||
container.append(block);
|
||||
}
|
||||
if (includePairAction && Object.hasOwn(roles, 'positive') && Object.hasOwn(roles, 'negative')) {
|
||||
|
||||
@@ -139,8 +139,8 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||||
.ca-tools dialog.prompt-sender { width: min(760px, calc(100% - 32px)); max-width: none; max-height: calc(100dvh - 32px); padding: 22px; border-color: var(--ca-line); border-radius: 12px; } /* PR D */
|
||||
.ca-tools dialog.prompt-sender::backdrop { background: #000b; } /* PR D */
|
||||
.ca-tools dialog.recorded-run-dialog { width: min(1100px, calc(100% - 32px)); max-width: calc(100% - 32px); max-height: calc(100dvh - 32px); padding: 22px; border-radius: 7px; } /* PR F */
|
||||
pre.prompt-text { background: none; padding: 0; border: 0; border-radius: 0; max-height: none; overflow: visible; } /* PR D: prompt text becomes `p.prompt-text` */
|
||||
.ca-tools .prompt-actions .ca-icon-button { min-width: 30px; } /* PR D: the concept's 28px prompt action would shrink the toolkit icon button */
|
||||
pre.prompt-text { background: none; padding: 0; border: 0; border-radius: 0; max-height: none; overflow: visible; } /* Library and history rebuilds: the Archive dialog and frozen field provenance keep `pre.prompt-text` */
|
||||
.prompt-text[tabindex]:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 3px; } /* production-only: a confirmed prompt stays keyboard-reachable (#249 D22) */
|
||||
@media (pointer: coarse) {
|
||||
.ca-tools .recorded-history .history-toolbar button { min-height: 44px; } /* PR F: concept coarse buttons are 40px */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user