fix(inspector): preserve fact states and touch targets

This commit is contained in:
Vito Sansevero
2026-10-18 05:04:45 -07:00
parent 7ffd1bc0e7
commit ab4c7cc96d
3 changed files with 60 additions and 2 deletions
+45
View File
@@ -98,6 +98,10 @@ test('full inspector shows six facts and scoped provenance',async({page,request}
await expect(panel.locator('.file-evidence')).toHaveText('Workflow prompt evidence');
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);
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']);
await expect(panel.locator('.generation-facts dd')).toHaveText(['9007199254740993','28','6.5','dpmpp_2m','karras','fixture-model.safetensors']);
await expect(panel).toContainText('Recipe tags');await expect(panel).toContainText('Recipe category');
@@ -189,3 +193,44 @@ test('full inspector labels container representations and releases action URLs',
await page.evaluate(()=>{window.renderFixture.view.dispose();window.renderFixture.view.dispose();window.renderFixture.scope.dispose();});await expect(panel).toBeEmpty();
expect(await page.evaluate(()=>window.renderFixture.revoked)).toEqual(await page.evaluate(()=>window.renderFixture.created));
});
test('full inspector distinguishes missing, limited and legacy unknown generation facts',async({page,request})=>{
await fullFixture(page,request);const panel=inspector(page);
await selectFile(page,'empty.png');await expect(panel.locator('.generation-facts dd')).toHaveText(Array(6).fill('Not recorded'));
await page.locator('article[data-image-key="0:architecture/study-01.png"] .image-open').click();await expect(panel.locator('.generation-facts dd')).toHaveText(Array(6).fill('Not recorded'));
await selectFile(page,'control-heavy.png');
const explanation='Configuration exceeds inspection limits; download the original file to inspect it locally.';
await expect(panel.getByText(explanation,{exact:true})).toHaveCount(1);await expect(panel.getByText(explanation,{exact:true})).toBeVisible();
for(const value of await panel.locator('.generation-facts dd').all())await expect(value).toHaveAccessibleDescription(explanation);
await page.evaluate(async()=>{
const {mountFileInspector}=await import('/prompt_manager/js/workspace/file-inspector.mjs');
const {toInspectionRecord}=await import('/prompt_manager/js/workspace/inspection-state.mjs');
const {createScope}=await import('/prompt_manager/js/shared/toolkit/core.js');
const container=document.createElement('section');container.id='legacy-facts';document.body.append(container);
window.legacyFactsScope=createScope();window.legacyFactsView=mountFileInspector(container,{parentScope:window.legacyFactsScope});
window.legacyFactsView.setState({record:toInspectionRecord({prompts:{}},{root_index:0,path:'legacy.png'})});
});
const legacy=page.locator('#legacy-facts');await expect(legacy.locator('.generation-facts dd')).toHaveText(Array(6).fill('Unresolved'));await expect(legacy).not.toContainText(explanation);await expect(legacy.locator('.generation-facts dd[aria-describedby]')).toHaveCount(0);
await page.evaluate(()=>{window.legacyFactsView.dispose();window.legacyFactsScope.dispose();});
});
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 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);}
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);
await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
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);
}
});
});
+10 -2
View File
@@ -74,7 +74,7 @@ function definitionList(entries, className) {
function factText(fact) {
if (fact?.status === 'known' && typeof fact.value === 'string') return fact.value;
if (fact?.status === 'ambiguous') return 'Ambiguous';
if (fact?.status === 'absent') return 'Not recorded';
if (fact?.status === 'not-recorded') return 'Not recorded';
return fact?.reason === 'projection-limited' ? 'Unavailable within inspection limits' : 'Unresolved';
}
function formatBytes(value) {
@@ -150,6 +150,14 @@ export function mountFileInspector(container, {sender, parentScope, inline = fal
} else if (!library) inner.append(el('p', 'Saved vocabulary is unavailable for this inspection response.', 'muted'));
const facts = definitionList(FACTS.map(([key, label]) => [label, factText(record.generation[key])]), 'generation-facts');
inner.append(facts);
if (FACTS.some(([key]) => record.generation[key]?.reason === 'projection-limited')) {
const explanation = el('p', 'Configuration exceeds inspection limits; download the original file to inspect it locally.', 'generation-limit muted');
explanation.id = `file-fact-limit-${++factReasonId}`;
FACTS.forEach(([key], index) => {
if (record.generation[key]?.reason === 'projection-limited') facts.children[index].querySelector('dd').setAttribute('aria-describedby', explanation.id);
});
inner.append(explanation);
}
const provenance = disclosure('File & provenance');
provenance.append(el('p', 'Current file at this source; historical bytes are not verified.', 'muted'));
if (file) provenance.append(definitionList([
@@ -217,7 +225,7 @@ export function mountFileInspector(container, {sender, parentScope, inline = fal
}
return {setState, dispose:() => scope.dispose()};
}
let downloadReasonId = 0;
let downloadReasonId = 0, factReasonId = 0;
/** Compatible standalone controller; Images owns a single shared controller instead. */
export function mountPromptInspector(container, {api, image, sender, parentScope}) {
+5
View File
@@ -17,6 +17,7 @@
.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; }
.generation-limit { font-size: 10px; line-height: 1.65; margin-block: 12px; }
.generation-facts dt, .file-facts dt { color: var(--ca-muted); font-size: 10px; }
.generation-facts dd, .file-facts dd { margin: 3px 0 0; }
.file-inspector details { border-block-start: 1px solid var(--ca-line); margin-block-start: 16px; padding-block-start: 13px; font-size: 12px; }
@@ -36,3 +37,7 @@
.file-inspector-footer .workspace-icon { inline-size: 14px; block-size: 14px; }
.file-status { padding: 17px; }
@media (max-width: 700px) { .ca-tools .file-cover { block-size: 200px; } }
@media (pointer: coarse) {
.ca-tools .file-inspector :is(button, a, summary) { min-inline-size: 40px; min-block-size: 40px; }
.ca-tools .file-inspector .prompt-actions button { inline-size: 36px; block-size: 36px; min-inline-size: 36px; min-block-size: 36px; }
}