fix: ignore stale gallery metadata responses

Fixes #174
This commit is contained in:
Vito Sansevero
2026-10-16 05:09:42 -07:00
parent 024130f0ee
commit ce44c43e9a
3 changed files with 112 additions and 1 deletions
+76
View File
@@ -0,0 +1,76 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const path = require('node:path');
function deferred() {
let resolve, reject;
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
return { promise, resolve, reject };
}
function fixture() {
const context = vm.createContext({ document: { addEventListener() {}, getElementById() {} },
window: {}, console: { error() {} }, escapeHtml: value => value });
const Manager = vm.runInContext(fs.readFileSync(path.join(__dirname, '../../web/js/gallery.js'), 'utf8') + '\nGalleryManager;', context);
const manager = Object.create(Manager.prototype);
const requests = new Map();
manager.extractImageMetadata = url => {
const pending = deferred();
requests.set(url, pending);
return pending.promise;
};
manager.displayMetadata = (metadata, panel) => {
panel.innerHTML = metadata?.positivePrompt || 'No metadata';
manager.currentMetadata = metadata;
};
manager.stopViewerSlideshow = () => {};
return { manager, requests, panel: { innerHTML: '', isConnected: true } };
}
test('a late response cannot overwrite the selected image or copy state', async () => {
const { manager, requests, panel } = fixture();
const first = manager.loadImageMetadata('old.png', panel);
const second = manager.loadImageMetadata('new.png', panel);
requests.get('new.png').resolve({ positivePrompt: 'new prompt' });
await second;
requests.get('old.png').resolve({ positivePrompt: 'old prompt' });
await first;
assert.equal(panel.innerHTML, 'new prompt');
assert.equal(manager.currentMetadata.positivePrompt, 'new prompt');
});
test('navigation clears old action data while the new request is pending', async () => {
const { manager, requests, panel } = fixture();
manager.currentMetadata = { positivePrompt: 'old prompt' };
const pending = manager.loadImageMetadata('new.png', panel);
assert.equal(manager.currentMetadata, null);
requests.get('new.png').resolve(null);
await pending;
assert.equal(manager.currentMetadata, null);
});
test('a stale error cannot replace newer metadata', async () => {
const { manager, requests, panel } = fixture();
const first = manager.loadImageMetadata('old.png', panel);
const second = manager.loadImageMetadata('new.png', panel);
requests.get('new.png').resolve({ positivePrompt: 'new prompt' });
await second;
requests.get('old.png').reject(new Error('old request failed'));
await first;
assert.equal(panel.innerHTML, 'new prompt');
});
for (const close of ['destroy viewer', 'remove modal']) {
test(`closing via ${close} prevents pending metadata from restoring action data`, async () => {
const { manager, requests, panel } = fixture();
const pending = manager.loadImageMetadata('old.png', panel);
if (close === 'destroy viewer') manager.destroyViewer();
else panel.isConnected = false;
requests.get('old.png').resolve({ positivePrompt: 'closed prompt' });
await pending;
assert.equal(manager.currentMetadata, null);
assert.notEqual(panel.innerHTML, 'closed prompt');
});
}
+25
View File
@@ -181,3 +181,28 @@ test('touch pinch zoom and rotation remain usable', async ({ page, isMobile }) =
await session.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await expect.poll(() => page.evaluate(() => Math.abs(gallery.viewer.imageData.rotate))).toBe(90);
});
test('metadata follows the selected image when responses finish out of order', async ({ page }) => {
await loadGallery(page);
await page.evaluate(() => {
delete gallery.loadImageMetadata;
window.metadataRequests = new Map();
gallery.extractImageMetadata = url => new Promise(resolve => metadataRequests.set(url, resolve));
gallery.openViewer(0);
});
await expect.poll(() => page.evaluate(() => metadataRequests.has('http://viewer.test/original/0.svg'))).toBe(true);
await page.evaluate(() => gallery.viewer.next());
await expect.poll(() => page.evaluate(() => metadataRequests.has('http://viewer.test/original/1.svg'))).toBe(true);
await page.evaluate(() => metadataRequests.get('http://viewer.test/original/1.svg')({ positivePrompt: 'Selected prompt' }));
await expect(page.locator('#metadata-content')).toContainText('Selected prompt');
await page.evaluate(() => metadataRequests.get('http://viewer.test/original/0.svg')({ positivePrompt: 'Stale prompt' }));
await expect(page.locator('#metadata-content')).toContainText('Selected prompt');
await expect(page.locator('#metadata-content')).not.toContainText('Stale prompt');
expect(await page.evaluate(() => gallery.currentMetadata.positivePrompt)).toBe('Selected prompt');
await page.evaluate(() => gallery.viewer.next());
await expect.poll(() => page.evaluate(() => metadataRequests.has('http://viewer.test/original/2.svg'))).toBe(true);
await page.keyboard.press('Escape');
await expect(page.locator('#metadata-sidebar')).toHaveCount(0);
await page.evaluate(() => metadataRequests.get('http://viewer.test/original/2.svg')({ positivePrompt: 'Closed prompt' }));
expect(await page.evaluate(() => gallery.currentMetadata)).toBeNull();
});
+11 -1
View File
@@ -583,6 +583,8 @@
}
destroyViewer({ keepSlideshow = false } = {}) {
this.metadataRequest = null;
this.currentMetadata = null;
if (!keepSlideshow) this.stopViewerSlideshow();
if (this.viewer) {
this.viewer.clearEnforceFocus();
@@ -647,6 +649,8 @@
}
},
hide: () => {
this.metadataRequest = null;
this.currentMetadata = null;
this.viewer?.clearEnforceFocus();
this.stopViewerSlideshow();
const sidebar = document.getElementById('metadata-sidebar');
@@ -1189,6 +1193,11 @@
}
async loadImageMetadata(imageUrl, container) {
const request = {};
this.metadataRequest = request;
this.currentMetadata = null;
container.textContent = 'Loading metadata...';
const isCurrent = () => this.metadataRequest === request && container.isConnected !== false;
try {
// First try to get linked prompt from database
let databasePrompt = null;
@@ -1254,8 +1263,9 @@
}
}
this.displayMetadata(metadata, container);
if (isCurrent()) this.displayMetadata(metadata, container);
} catch (error) {
if (!isCurrent()) return;
console.error('Error loading metadata:', error);
container.innerHTML = `
<div class="bg-pm-error-tint border border-pm-error rounded-pm-md p-4">