diff --git a/tests/browser/workspace-history.spec.mjs b/tests/browser/workspace-history.spec.mjs index ee5ac6d..e2b94ac 100644 --- a/tests/browser/workspace-history.spec.mjs +++ b/tests/browser/workspace-history.spec.mjs @@ -458,6 +458,26 @@ test('frozen Effective pair uses the shared acknowledged sender and Escape close } finally {await host.evaluate(()=>window.historyHost.dispose());await host.close();} }); +test('the inline current file names its prompts apart from the frozen run fields',async({page})=>{ + // The current file resolves its own prompts, as a real host's saved PNG does, so the dialog holds two prompt texts per role. + await page.route(url=>url.pathname.endsWith('/inspection'),async route=>{const response=await route.fetch();const body=await response.json(); + body.prompts={positive:{status:'resolved',text:'current file positive',source:'graph-inferred'},negative:{status:'resolved',text:'current file negative',source:'graph-inferred'}}; + await route.fulfill({response,json:body});}); + 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')); + await dialog(page).locator('[data-receipt]').first().getByRole('button',{name:'View current file',exact:true}).click(); + await expect(page.getByRole('dialog',{name:'Image preview',exact:true})).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('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); +}); + test('pagehide tears down dialogs and persisted reentry revalidates before enabling recorded actions',async({page})=>{ 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).locator('[data-receipt]').first().getByRole('button',{name:'View current file',exact:true}).click();await expect(page.getByRole('dialog',{name:'Image preview',exact:true})).toBeVisible(); diff --git a/tests/comfyui/execution-history.spec.mjs b/tests/comfyui/execution-history.spec.mjs index cb85c24..a5332d1 100644 --- a/tests/comfyui/execution-history.spec.mjs +++ b/tests/comfyui/execution-history.spec.mjs @@ -243,7 +243,7 @@ test.describe('public chronological history with the actual acknowledged host br const frozen = attempts[0].context; for (const mode of ['Raw', 'Effective']) { await run.getByRole('button', { name: mode, exact: true }).click(); - for (const [role, value] of [['Positive', frozen.positive[mode.toLowerCase()]], ['Negative', frozen.negative[mode.toLowerCase()]]]) expect(await run.getByRole('region', { name: `${role} prompt`, exact: true }).locator('pre').evaluate(node => node.textContent)).toBe(value); + for (const [role, value] of [['Positive', frozen.positive[mode.toLowerCase()]], ['Negative', frozen.negative[mode.toLowerCase()]]]) expect(await run.getByRole('region', { name: 'Frozen run prompt texts', exact: true }).getByRole('region', { name: `${role} prompt`, exact: true }).locator('pre').evaluate(node => node.textContent)).toBe(value); await page.evaluate(async id => {const { app } = await import('/scripts/app.js');const node = app.graph.getNodeById(id);node.widgets.find(w => w.name === 'text').value = 'Before send';window.chronologyEffects = { queue: 0, load: 0 };window.chronologyOriginals = {};for (const [method, key] of [['queuePrompt', 'queue'], ['loadGraphData', 'load']]) {const original = app[method];window.chronologyOriginals[method] = original;app[method] = function(...args) {window.chronologyEffects[key]++;return original.apply(this, args);};}}, destination); try { await run.getByRole('button', { name: 'Send prompt pair', exact: true }).click();const sender = workspace.getByRole('dialog', { name: 'Send to ComfyUI', exact: true }); diff --git a/tests/js/prompt-blocks.test.js b/tests/js/prompt-blocks.test.js index b32c6be..803e04a 100644 --- a/tests/js/prompt-blocks.test.js +++ b/tests/js/prompt-blocks.test.js @@ -130,3 +130,16 @@ test('class names in status lines render as text, never markup',()=>{ assert.equal(f.walk(container,node=>Object.hasOwn(node,'innerHTML')).length,0); }finally{scope.dispose();f.restore();} }); + +test('a context label names nested prompt regions and actions apart from the frozen blocks while the visible heading stays',()=>{ + const f=dom(),scope=createScope(),container=new f.Element('div'); + 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); + 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']); + 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();} +}); diff --git a/web/js/workspace/file-inspector.mjs b/web/js/workspace/file-inspector.mjs index a6f048f..f52c433 100644 --- a/web/js/workspace/file-inspector.mjs +++ b/web/js/workspace/file-inspector.mjs @@ -85,7 +85,7 @@ function formatBytes(value) { return value === null || value === undefined ? null : value >= 1048576 ? `${(value / 1048576).toFixed(1)} MB` : `${Math.max(1, Math.round(value / 1024))} KB`; } -export function mountFileInspector(container, {sender, parentScope, inline = false, onPreview, onRetry, onRefresh, onInteraction = () => {}}) { +export function mountFileInspector(container, {sender, parentScope, inline = false, promptContext = '', onPreview, onRetry, onRefresh, onInteraction = () => {}}) { if (parentScope.signal.aborted) return {setState() {}, dispose() {}}; const scope = childScope(parentScope); let contentScope; @@ -145,7 +145,7 @@ export function mountFileInspector(container, {sender, parentScope, inline = fal } if (chips.childNodes.length) inner.append(chips); const prompts = el('div'); inner.append(prompts); - const blocks = mountPromptBlocks(prompts, {sender, parentScope:contentScope, includePairAction:false}); + const blocks = mountPromptBlocks(prompts, {sender, parentScope:contentScope, includePairAction:false, contextLabel:promptContext}); blocks.setState({prompts:record.prompts}); contentScope.cleanup(blocks.dispose); if (library?.status === 'linked') { const vocabulary = el('div', undefined, 'file-vocabulary'); diff --git a/web/js/workspace/prompt-blocks.mjs b/web/js/workspace/prompt-blocks.mjs index f3a3bcb..5c3db6e 100644 --- a/web/js/workspace/prompt-blocks.mjs +++ b/web/js/workspace/prompt-blocks.mjs @@ -41,7 +41,11 @@ function childScope(parentScope) { return scope; } -export function mountPromptBlocks(container, {prompts, sender, parentScope, roles:visibleRoles = ['positive', 'negative'], onRetry, includePairAction = true}) { +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 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); container.classList.add('prompt-inspector'); @@ -87,7 +91,7 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role const title = `${role[0].toUpperCase() + role.slice(1)} prompt`; const record = state.prompts?.[role]; const block = el('section', undefined, `prompt-block prompt-${role}`); - block.setAttribute('aria-label', title); + block.setAttribute('aria-label', contextLabel ? named(role) : title); const heading = el('div', undefined, 'prompt-block-heading'); heading.append(el('h3', title)); block.append(heading); @@ -101,12 +105,12 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role block.append(text); } const actions = el('div', undefined, 'prompt-actions'); - const send = createSendButton(`Send ${role} prompt`); + const send = createSendButton(`Send ${action(role)}`); send.dataset.sendRole = role; send.disabled = !actionsEnabled; const copy = createButton('', 'ca-icon-button'); - copy.setAttribute('aria-label', `Copy ${role} prompt`); - copy.title = `Copy ${role} prompt`; + copy.setAttribute('aria-label', `Copy ${action(role)}`); + copy.title = `Copy ${action(role)}`; copy.append(createWorkspaceIcon('copy')); copy.dataset.copyRole = role; copy.disabled = !actionsEnabled; diff --git a/web/js/workspace/recorded-run.mjs b/web/js/workspace/recorded-run.mjs index 8bfb842..29b815b 100644 --- a/web/js/workspace/recorded-run.mjs +++ b/web/js/workspace/recorded-run.mjs @@ -68,7 +68,7 @@ export function mountRecordedRun(container, {api, controller, sender, parentScop const receiptNodes = new Map(); const evidenceValues = new Map(); const blocks = mountPromptBlocks(prompts, {sender, parentScope:local}); - const inspector = mountFileInspector(inspectorContainer, {sender, parentScope:local, inline:true, onRetry:() => inspection.retry(), onPreview:() => showViewer(), onRefresh:refreshCurrent}); + const inspector = mountFileInspector(inspectorContainer, {sender, parentScope:local, inline:true, promptContext:'Current file', onRetry:() => inspection.retry(), onPreview:() => showViewer(), onRefresh:refreshCurrent}); function selectedReceipt() { const selected = latest.selection.receipt; return selected && latest.outputs.items.find(item => occurrenceKey(item.occurrence) === occurrenceKey(selected));