diff --git a/tests/browser/workspace-tagging.spec.mjs b/tests/browser/workspace-tagging.spec.mjs index aaee7195..e9ad4a8b 100644 --- a/tests/browser/workspace-tagging.spec.mjs +++ b/tests/browser/workspace-tagging.spec.mjs @@ -4,7 +4,7 @@ import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs'; import { test, expect } from './fixtures.mjs'; import { saveImageTags } from './concept-inventory.mjs'; -import { taggingInventory } from './tagging-canonical-comparison.mjs'; +import { FIXTURE_INSTRUCTIONS_ID, taggingInventory } from './tagging-canonical-comparison.mjs'; const images = '/prompt_manager/workspace#/images'; const MODULE = '/prompt_manager/js/workspace/image-tagging.mjs'; @@ -878,9 +878,13 @@ test('the saved instructions picker fills, edits one-off and resets per engine ( await expect(pick).toHaveAttribute('id', 'image-tag-instructions-pick'); await expect(text).toHaveValue('Describe the image'); await expect(note).toBeHidden(); + // A keyboard pick (the select focused) keeps focus on the select the fields are re-rendered with. + await pick.focus(); await pick.selectOption('builtin'); await expect(text).toHaveValue(builtin); + await expect(pick).toBeFocused(); await text.fill('Only the sky.'); + await expect(text).toHaveAttribute('aria-describedby', 'image-tag-instructions-pick-note'); await expect(pick.locator('option:checked')).toHaveText('Edited for this run'); await expect(pick.locator('option[value="edited"]')).toBeDisabled(); await expect(note).toHaveText('Edits apply to this run only. Manage saved instructions in System › Auto-tagging.'); @@ -896,7 +900,7 @@ test('the saved instructions picker fills, edits one-off and resets per engine ( await text.fill('Edited again.'); await model(page).selectOption('hf'); await expect(text).toHaveValue('Describe the image'); - await expect(pick).toHaveValue('d35c71be0a1e'); + await expect(pick).toHaveValue(FIXTURE_INSTRUCTIONS_ID); await model(page).selectOption('wd14-vit'); await expect(pick).toHaveCount(0); await expect(text).toHaveCount(0); diff --git a/web/js/workspace/image-tagging-state.mjs b/web/js/workspace/image-tagging-state.mjs index 97331d51..c6474e1e 100644 --- a/web/js/workspace/image-tagging-state.mjs +++ b/web/js/workspace/image-tagging-state.mjs @@ -243,8 +243,6 @@ async function readDefaults(api, signal) { defaults: { general_threshold: unit(value.wd14_general_threshold) ? value.wd14_general_threshold : DEFAULTS.general_threshold, character_threshold: unit(value.wd14_character_threshold) ? value.wd14_character_threshold : DEFAULTS.character_threshold, - // #431: the instructions come from the saved entries (`choices`), not `default_prompt`. - instructions: DEFAULTS.instructions, suggestion_limit: limit(value.suggestion_limit) ? value.suggestion_limit : DEFAULTS.suggestion_limit, }, models: object(value.models) ? value.models : null, diff --git a/web/js/workspace/image-tagging.mjs b/web/js/workspace/image-tagging.mjs index e3ec1e46..6b77ce77 100644 --- a/web/js/workspace/image-tagging.mjs +++ b/web/js/workspace/image-tagging.mjs @@ -150,6 +150,10 @@ export function renderModelPanel({scope, controller, heading = 'Auto-tag this im } pick.value = state.instructionsEdited ? EDITED_VALUE : state.instructionId; editedNote.hidden = !state.instructionsEdited; + // The note describes the textarea while it shows. + const area = inputs.get('instructions'); + if (state.instructionsEdited) area?.setAttribute('aria-describedby', editedNote.id); + else area?.removeAttribute('aria-describedby'); } function renderOptions(state) { @@ -174,6 +178,7 @@ export function renderModelPanel({scope, controller, heading = 'Auto-tag this im inputs.set('instructions', area); rows.push(field('Tagging instructions', area, OPTION_IDS.instructions)); editedNote = el('p', EDITED_NOTE, 'small instructions-edited'); + editedNote.id = `${pickId}-note`; rows.push(editedNote); fillPick(state); } @@ -195,7 +200,10 @@ export function renderModelPanel({scope, controller, heading = 'Auto-tag this im shownModel = state.model; shownOptions = state.optionsRevision; select.value = state.model; + // A pick re-renders the fields; keyboard focus stays on the select it was made with. + const picking = pick !== null && document.activeElement === pick; renderOptions(state); + if (picking) pick?.focus(); } else if (pick && state.instructionsEdited !== shownEdited) { // Typing never re-renders the fields (I1): only the select and the note follow an edit. fillPick(state);