Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
978f8f9e40 |
@@ -221,6 +221,33 @@ test('tag image from review saves tags and the item remains pending', async ({ p
|
||||
await expect(dialog).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('#515: tags typed before the image observation answers are saved', async ({ page, request }) => {
|
||||
await seed(request, 1);
|
||||
await open(page);
|
||||
// Hold the dialog's observation so the typing lands while it is pending (a slow host or WebKit in CI).
|
||||
let release;
|
||||
const held = new Promise(resolve => { release = resolve; });
|
||||
const matcher = '**/prompt_manager/image-annotations/observe';
|
||||
let reachedObserve;
|
||||
const reached = new Promise(resolve => { reachedObserve = resolve; });
|
||||
await page.route(matcher, async route => { reachedObserve(); await held; await route.continue(); });
|
||||
await panel(page).getByRole('button', { name: 'Tag image', exact: true }).click();
|
||||
const dialog = page.getByRole('dialog', { name: 'Tag image', exact: true });
|
||||
await reached;
|
||||
const field = dialog.getByRole('textbox', { name: 'Image tags · edit manually', exact: true });
|
||||
await field.fill('dunes');
|
||||
const save = dialog.getByRole('button', { name: 'Save manual tags', exact: true });
|
||||
await expect(save).toHaveAttribute('aria-disabled', 'true');
|
||||
const answered = page.waitForResponse(item => item.url().endsWith('/image-annotations/observe'));
|
||||
release();
|
||||
await answered;
|
||||
await expect(save).not.toHaveAttribute('aria-disabled', 'true');
|
||||
await expect(field).toHaveValue('dunes');
|
||||
await save.click();
|
||||
await expect(toast(page)).toHaveText('1 tags saved for review_0000.png. Prompt association still awaits review.');
|
||||
await page.unroute(matcher);
|
||||
});
|
||||
|
||||
test('an empty queue shows the empty notice only', async ({ page, request }) => {
|
||||
await seed(request, 0);
|
||||
await page.goto(review);
|
||||
|
||||
@@ -876,3 +876,63 @@ test('retry after a changed storage, file or expired capture observes again and
|
||||
await settle();
|
||||
assert.equal(c.state.manual, 'Minimal, warm_light');
|
||||
});
|
||||
|
||||
test('#515: text typed while the first observation is pending is kept, after the saved tags', async () => {
|
||||
const { createImageTaggingController } = await load();
|
||||
const answers = [deferred(), deferred()];
|
||||
let n = 0;
|
||||
const h = harness(defaultRoutes({ [`POST ${OBSERVE}`]: () => answers[n++].promise }));
|
||||
// An untagged file: the typed text is the whole draft and Save sends it.
|
||||
const bare = createImageTaggingController({ api: h.api, target: TARGET, source: 'review' });
|
||||
const first = bare.open();
|
||||
bare.setManual('dunes');
|
||||
answers[0].resolve(response(200, observed({ tags: [], tag_revision: 0 })));
|
||||
await first;
|
||||
await settle();
|
||||
assert.equal(bare.state.phase, 'ready');
|
||||
assert.equal(bare.state.manual, 'dunes');
|
||||
assert.equal(bare.state.statusText, '', 'nothing changed elsewhere');
|
||||
await bare.save();
|
||||
await settle();
|
||||
assert.deepEqual(h.of('PUT', TAGS).at(-1).body.tags, ['dunes']);
|
||||
// A tagged file: its saved tags lead, so a Save never drops tags the user had not seen yet.
|
||||
const tagged = createImageTaggingController({ api: h.api, target: TARGET });
|
||||
const second = tagged.open();
|
||||
tagged.setManual('dunes, minimal');
|
||||
answers[1].resolve(response(200, observed()));
|
||||
await second;
|
||||
await settle();
|
||||
assert.equal(tagged.state.manual, 'Minimal, warm_light, dunes');
|
||||
assert.equal(tagged.state.statusText, '');
|
||||
});
|
||||
|
||||
test('#515: a kept draft takes the text typed while the reopen observes', async () => {
|
||||
const { c } = await opened({ [`PUT ${TAGS}`]: () => fail(409, 'file-changed', 'The file changed.') });
|
||||
c.setManual('kept draft');
|
||||
await c.save();
|
||||
await settle();
|
||||
const reopened = c.retry();
|
||||
c.setManual('kept draft, later');
|
||||
await reopened;
|
||||
await settle();
|
||||
assert.equal(c.state.phase, 'ready');
|
||||
assert.equal(c.state.manual, 'kept draft, later');
|
||||
});
|
||||
|
||||
test('#515: text typed before Retry after a failed first observation follows the saved tags', async () => {
|
||||
const { createImageTaggingController } = await load();
|
||||
let n = 0;
|
||||
const h = harness(defaultRoutes({
|
||||
[`POST ${OBSERVE}`]: () => (n++ === 0 ? fail(500, 'internal', 'boom') : response(200, observed())),
|
||||
}));
|
||||
const c = createImageTaggingController({ api: h.api, target: TARGET });
|
||||
await c.open();
|
||||
await settle();
|
||||
assert.equal(c.state.phase, 'observe-error');
|
||||
c.setManual('dunes');
|
||||
await c.retry();
|
||||
await settle();
|
||||
assert.equal(c.state.phase, 'ready');
|
||||
assert.equal(c.state.manual, 'Minimal, warm_light, dunes');
|
||||
assert.equal(c.state.statusText, '');
|
||||
});
|
||||
|
||||
@@ -317,6 +317,9 @@ export function createImageTaggingController({api, target, source = 'images', on
|
||||
// A save in flight: closing hands the capture release to its completion.
|
||||
let saving = false;
|
||||
let observedOnce = false;
|
||||
// #515: the user typed since the last observation started (or before the first one answered), so the answer
|
||||
// keeps that text.
|
||||
let typed = false;
|
||||
|
||||
function set(patch) {
|
||||
if (closed) return;
|
||||
@@ -345,7 +348,8 @@ export function createImageTaggingController({api, target, source = 'images', on
|
||||
async function open({keepDraft = false} = {}) {
|
||||
if (closed) return;
|
||||
const serial = ++openSerial;
|
||||
const draft = keepDraft ? state.manual : null;
|
||||
// A reopen after an observation starts from its answer; text typed before any observation is kept (#515).
|
||||
if (observedOnce) typed = false;
|
||||
const seenRevision = state.tagRevision;
|
||||
stopGenerating();
|
||||
releaseCapture();
|
||||
@@ -385,15 +389,19 @@ export function createImageTaggingController({api, target, source = 'images', on
|
||||
const savedModel = !chosen && !modelPicked && read.model ? {model: read.model} : {};
|
||||
observedOnce = true;
|
||||
const {width, height, media_type: mediaType, preview_url: previewUrl, generate} = value.capture;
|
||||
// A kept draft stays as typed unless another writer changed the set meanwhile: then the set leads it (I2).
|
||||
const changed = draft !== null && value.tag_revision !== seenRevision;
|
||||
// The draft is read now, so text typed while observing is kept (#515). A kept draft stays as typed unless
|
||||
// another writer changed the set meanwhile: then the set leads it (I2). Text typed before the user saw any
|
||||
// saved tags follows them, so a Save never drops tags it never showed.
|
||||
const draft = keepDraft || typed ? state.manual : null;
|
||||
const changed = keepDraft && value.tag_revision !== seenRevision;
|
||||
const unseen = draft !== null && !keepDraft;
|
||||
const reason = generate.supported ? '' : (generate.reason || '');
|
||||
set({
|
||||
phase: 'ready',
|
||||
image: {filename: value.filename, title: value.title, width, height, mediaType, previewUrl,
|
||||
dimensions: width && height ? `${width} × ${height}` : null},
|
||||
savedTags: value.tags.slice(), tagRevision: value.tag_revision,
|
||||
manual: draft === null ? value.tags.join(', ') : changed ? mergeCurrent(value.tags, draft) : draft,
|
||||
manual: draft === null ? value.tags.join(', ') : changed || unseen ? mergeCurrent(value.tags, draft) : draft,
|
||||
draftRevision: state.draftRevision + 1,
|
||||
...savedModel, options: {...defaults, ...chosen?.options}, optionsRevision: state.optionsRevision + 1,
|
||||
generateSupported: generate.supported, generateReason: generate.reason,
|
||||
@@ -402,6 +410,7 @@ export function createImageTaggingController({api, target, source = 'images', on
|
||||
}
|
||||
|
||||
function setManual(text) {
|
||||
typed = true;
|
||||
set({manual: typeof text === 'string' ? text : ''});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user